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

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

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

REACT- Displaying and filtering specific data

I want to display by default only data where the status are Pending and Not started. For now, all data are displayed in my Table with
these status: Good,Pending, Not started (see the picture).
But I also want to have the possibility to see the Good status either by creating next to the Apply button a toggle switch : Show good menus, ( I've made a function Toggle.jsx), which will offer the possibility to see all status included Good.
I really don't know how to do that, here what I have now :
export default function MenuDisplay() {
const { menuId } = useParams();
const [selected, setSelected] = useState({});
const [hidden, setHidden] = useState({});
const [menus, setMenus] = useState([]);
useEffect(() => {
axios.post(url,{menuId:parseInt(menuId)})
.then(res => {
console.log(res)
setMenus(res.data.menus)
})
.catch(err => {
console.log(err)
})
}, [menuId]);
// If any row is selected, the button should be in the Apply state
// else it should be in the Cancel state
const buttonMode = Object.values(selected).some((isSelected) => isSelected)
? "apply"
: "cancel";
const rowSelectHandler = (id) => (checked) => {
setSelected((selected) => ({
...selected,
[id]: checked
}));
};
const handleClick = () => {
if (buttonMode === "apply") {
// Hide currently selected items
const currentlySelected = {};
Object.entries(selected).forEach(([id, isSelected]) => {
if (isSelected) {
currentlySelected[id] = isSelected;
}
});
setHidden({ ...hidden, ...currentlySelected });
// Clear all selection
const newSelected = {};
Object.keys(selected).forEach((id) => {
newSelected[id] = false;
});
setSelected(newSelected);
} else {
// Select all currently hidden items
const currentlyHidden = {};
Object.entries(hidden).forEach(([id, isHidden]) => {
if (isHidden) {
currentlyHidden[id] = isHidden;
}
});
setSelected({ ...selected, ...currentlyHidden });
// Clear all hidden items
const newHidden = {};
Object.keys(hidden).forEach((id) => {
newHidden[id] = false;
});
setHidden(newHidden);
}
};
const matchData = (
menus.filter(({ _id }) => {
return !hidden[_id];
});
const getRowProps = (row) => {
return {
style: {
backgroundColor: selected[row.values.id] ? "lightgrey" : "white"
}
};
};
const data = [
{
Header: "id",
accessor: (row) => row._id
},
{
Header: "Name",
accessor: (row) => (
<Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>{row.name}</Link>
)
},
{
Header: "Description",
//check current row is in hidden rows or not
accessor: (row) => row.description
},
{
Header: "Status",
accessor: (row) => row.status
},
{
Header: "Dishes",
//check current row is in hidden rows or not
accessor: (row) => row.dishes,
id: "dishes",
Cell: ({ value }) => value && Object.values(value[0]).join(", ")
},
{
Header: "Show",
accessor: (row) => (
<Toggle
value={selected[row._id]}
onChange={rowSelectHandler(row._id)}
/>
)
}
];
const initialState = {
sortBy: [
{ desc: false, id: "id" },
{ desc: false, id: "description" }
],
hiddenColumns: ["dishes", "id"]
};
return (
<div>
<button type="button" onClick={handleClick}>
{buttonMode === "cancel" ? "Cancel" : "Apply"}
</button>
<Table
data={matchData}
columns={data}
initialState={initialState}
withCellBorder
withRowBorder
withSorting
withPagination
rowProps={getRowProps}
/>
</div>
);
}
Here my json from my api for menuId:1:
[
{
"menuId": 1,
"_id": "123ml66",
"name": "Pea Soup",
"description": "Creamy pea soup topped with melted cheese and sourdough croutons.",
"dishes": [
{
"meat": "N/A",
"vegetables": "pea"
}
],
"taste": "Good",
"comments": "3/4",
"price": "Low",
"availability": 0,
"trust": 1,
"status": "Pending",
"apply": 1
},
//...other data
]
Here my CodeSandbox
Here a picture to get the idea:
Here's the second solution I proposed in the comment:
// Setting up toggle button state
const [showGood, setShowGood] = useState(false);
const [menus, setMenus] = useState([]);
// Simulate fetch data from API
useEffect(() => {
async function fetchData() {
// After fetching data with axios or fetch api
// We process the data
const goodMenus = dataFromAPI.filter((i) => i.taste === "Good");
const restOfMenus = dataFromAPI.filter((i) => i.taste !== "Good");
// Combine two arrays into one using spread operator
// Put the good ones to the front of the array
setMenus([...goodMenus, ...restOfMenus]);
}
fetchData();
}, []);
return (
<div>
// Create a checkbox (you can change it to a toggle button)
<input type="checkbox" onChange={() => setShowGood(!showGood)} />
// Conditionally pass in menu data based on the value of toggle button "showGood"
<Table
data={showGood ? menus : menus.filter((i) => i.taste !== "Good")}
/>
</div>
);
On ternary operator and filter function:
showGood ? menus : menus.filter((i) => i.taste !== "Good")
If button is checked, then showGood's value is true, and all data is passed down to the table, but the good ones will be displayed first, since we have processed it right after the data is fetched, otherwise, the menus that doesn't have good status is shown to the UI.
See sandbox for the simple demo.

Patch multiple id's with one request with React Query

I have a very basic prototype of app that allows to book a seat. User selects the seat/seats, clicks book, patch request with available: false is sent to the fake api (json-server) with React Query, library invalidates the request and immediately shows response from the server.
Database structure looks like this:
{
"hallA": [
{
"id": 1,
"seat": 1,
"available": false
},
{
"id": 2,
"seat": 2,
"available": true
},
{
"id": 3,
"seat": 3,
"available": false
}
]
}
and the logic for selecting, booking seats looks like this:
const App = () => {
const { data, isLoading } = useGetHallLayout("hallA");
const [selected, setSelected] = useState<
{ id: number; seat: number; available: boolean }[]
>([]);
const handleSelect = useCallback(
(seat: { id: number; seat: number; available: boolean }) => {
const itemIdx = selected.findIndex((element) => element.id === seat.id);
if (itemIdx === -1) {
setSelected((prevState) => [
...prevState,
{ id: seat.id, seat: seat.seat, available: !seat.available },
]);
} else {
setSelected((prevState) =>
prevState.filter((element) => element.id !== seat.id)
);
}
},
[selected]
);
const takeSeat = useTakeSeat({
onSuccess: () => {
useGetHallLayout.invalidate();
},
});
const sendRequest = useCallback(() => {
selected.forEach((item) =>
takeSeat.mutateAsync({ id: item.id, hall: "hallA" })
);
setSelected([]);
}, [selected, takeSeat]);
return (
<>
{!isLoading && (
<ConcertHall
layout={data}
onSeatSelect={handleSelect}
activeSeats={selected}
/>
)}
<button disabled={isLoading} onClick={sendRequest}>
Take selected
</button>
</>
);
};
Queries look like this:
export const useGetHallLayout = (hall: string) => {
const { data, isLoading } = useQuery(["hall"], () =>
axios.get(`http://localhost:3000/${hall}`).then((res) => res.data)
);
return { data, isLoading };
};
export const useTakeSeat = (options?: UseMutationOptions<unknown, any, any>) =>
useMutation(
(data: { hall: string; id: number }) =>
axios.patch(`http://localhost:3000/${data.hall}/${data.id}`, {
available: false,
}),
{
...options,
}
);
useGetHallLayout.invalidate = () => {
return queryClient.invalidateQueries("hall");
};
The problem of the above code is that I perform very expensive operation of updating each id in a for each loop (to available: false) and query invalidates it after each change not once all of them are updated.
The question is: is there any better way to do this taking into account the limitations of json-server? Any batch update instead of sending request to each and every id seperately? Maybe some changes in a logic?
Thanks in advance
You can certainly make one mutation that fires of multiple requests, and returns the result with Promise.all or Promise.allSettled. Something like:
useMutation((seats) => {
return Promise.allSettled(seats.map((seat) => axios.patch(...))
})
then, you would have one "lifecycle" (loading / error / success) for all queries together, and onSuccess will only be called once.
Another gotcha I'm seeing is that you'd really want the hall string to be part of the query key:
- useQuery(["hall"], () =>
+ useQuery(["hall", hall], () =>

Fetching , rendering and sorting data in Reactjs

How can i fetch data from a json file and render them in an ascending order? For example let´s say i have this json file
[{
"id": 0,
"name": "Vernon Dunham",
"company": "Qualcore",
"email": "vernon.dunham#qualcore.com"
}, {
"id": 1,
"name": "Dori Neal",
"company": "Sunopia",
"email": "dori.neal#sunopia.com"
}, {
"id": 2,
"name": "Rico Muldoon",
"company": "Airconix",
"email": "rico.muldoon#airconix.com"
}
and i want to render the data with only Id and name in ascending order.
I tried coding it in different ways but since i am still a beginner i can´t really figure it out. Thank you so much
You can use .map and .sort:
const data = [{
"id": 0,
"name": "Vernon Dunham",
"company": "Qualcore",
"email": "vernon.dunham#qualcore.com"
}, {
"id": 1,
"name": "Dori Neal",
"company": "Sunopia",
"email": "dori.neal#sunopia.com"
}, {
"id": 2,
"name": "Rico Muldoon",
"company": "Airconix",
"email": "rico.muldoon#airconix.com"
}]
const sorted = data.map(d => ({id: d.id, name: d.name})).sort((el1, el2) => el1.id - el2.id)
console.log(sorted)
Once you set the new sorted and mapped array to a state variable, you can map over it to render.
Example
//Functional Component:
const Example = ({data}) => {
const [sortedData, setSortedData] = useState([])
useEffect(() => {
if(data) setSortedData(data
.map(d => ({id: d.id, name: d.name}))
.sort((el1, el2) => el1.id - el2.id)))
}, [data])
return(
sortedData?.map(element => <div key={element.id}> {element.name} </div>)
)
}
You can short your array data using the .sort method.
App.js
import React, { useState, useEffect } from "react";
import Data from "./data.json";
function App() {
const [data, setData] = useState();
const sortDataByField = (array, field) => {
return array.sort((a, b) =>
a[field].toString().localeCompare(b[field].toString())
);
};
useEffect(() => {
const sortData = sortDataByField(Data, "name");
setData(sortData);
}, []);
return (
<div className="App">
{data?.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
export default App;
data.json
[{
"id": 0,
"name": "Vernon Dunham",
"company": "Qualcore",
"email": "vernon.dunham#qualcore.com"
}, {
"id": 1,
"name": "Dori Neal",
"company": "Sunopia",
"email": "dori.neal#sunopia.com"
}, {
"id": 2,
"name": "Rico Muldoon",
"company": "Airconix",
"email": "rico.muldoon#airconix.com"
}]
Import and set up a useState hook
import React, {usesState} from 'react'
const [companies, setCompanies] = useState([])
fetch the data
const data = fetch('/api/v1/companies', {method: 'GET'})
sort first to order, then map to only get id and name
const sorted = data.sort((a, b)=>{return a.name > b.name ? 1 : -1})
.map(({id, name}) => {return {id, name}})
/* this return only id and name sorted by name
*
* if you wanna sort by id, do this:
* .sort((a, b) => a.id - b.id)
*/
When you done sorting and mapping, store it to that useState hook so that you can render it:
setCompanies(sorted)
Then render it, also prevent errors by checking if you state has data before rendering:
companies &&
companies.map(({id, name}) => <h1 key={id}>{name}</h1>)
// or
companies
? companies.map(({id, name}) => <h1 key={id}>{name}</h1>)
: <p>No data</p>
All code
import React, {usesState, useEffect} from 'react'
export default App = () =>{
const [companies, setCompanies] = useState([])
useEffect(()=>{
fetchData()
}, [])
const fetchData = async () => {
try{
const data = fetch('/api/v1/companies', {method: 'GET'})
if(data){
const sorted = data.sort((a, b)=>{return a.name > b.name ? 1 : -1})
.map(({id, name}) => {return {id, name}})
setCompanies(sorted)
}
else{
console.log('No data')
}
}
catch (err){
console.log(err)
}
}
return(
<div>
companies &&
companies.map(({id, name}) => <h1 key={id}>{name}</h1>)
</div>
)
}

Refactoring from promise to async await and use pagination

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

Resources