How to fetch with parameters using React Query? - reactjs

For the sake of this question let's first assume existence of such entity:
export interface Event {
id: number;
date: Date;
}
Then let's assume there's backend with such endpoints:
GET /events -> returns all events
GET /events?startDate=dateA&endDate=dateB -> returns all events between dateA and dateB
I create hook containing 4 methods (one for each CRUD operation) in my frontend code like this:
export function useEvents() {
const getEvents() = async () => {
const response = await axios.get(`events`);
return response.data;
}
const postEvent()...
const updateEvent()...
const deleteEvent()...
const query = useQuery('events', getEvents);
const postMutation = ...
const updateMutation = ...
const deleteMutation = ...
return { query, postMutation, updateMutation, deleteMutation }
}
This architecture works like a charm but I got to the point where I would like to conditionaly fetch events based on currently chosen month in my Calendar.tsx component.
How would I inject this information into useQuery() and getEvents()?

the query key should contain all "dependencies" that you need for your fetch. This is documented in the official docs here, and I've also blogged about it here.
So, in short:
const getEvents(month) = async () => {
const response = await axios.get(`events/${month}`);
return response.data;
}
const query = useQuery(['events', month], () => getEvents(month));
The good thing is that react-query will always refetch when the key changes, so data for every month is cached separately, and if the month changes, you'll get a fetch with that month.

Related

How to build query in React MERN stack

I use the MERN stack and I want to create something like this on the frontend: http://localhost:3000/products?color=0000&sort=latest&category=man etc.
I'm using the redux toolkit and my getProductSlice looks like this:
export const getProducts = createAsyncThunk(
'product/get',
async ({ page, sort, localValue, colorFilter, categoryFilter }, thunkAPI) => {
let colorStr = colorFilter.substring(1);
let url = `/api/v1/products?page=${page}&sort=${sort}&color=${colorStr}&cat=${categoryFilter}`;
if (localValue) {
url = url + `&search=${localValue}`;
}
try {
const { data } = await axios.get(url);
return data;
} catch (error) {
const message = error.response.data.msg;
return thunkAPI.rejectWithValue(message);
}
}
);
Every filter works great, in the backend I return products depending on which filter is applied. I dont know how to build query in frontend and when i enter that url i want to filters to be applied based on url.
To build a query string in the frontend and apply filters based on the query string in the URL, you can do the following:
1 Parse the query string from the URL using the URLSearchParams API:
const searchParams = new URLSearchParams(window.location.search);
Extract the values of the filters from the query string using the get method of URLSearchParams:
const color = searchParams.get('color');
const sort = searchParams.get('sort');
const category = searchParams.get('category');
Use the extracted filter values to create a payload object for the getProducts async thunk:
const payload = {
page: 1, // or whatever page you want to start from
sort,
localValue: '', // or whatever value you want to use for the localValue filter
colorFilter: color,
categoryFilter: category
};
Dispatch the getProducts async thunk with the payload object:
dispatch(getProducts(payload));
This should apply the filters based on the query string in the URL when the getProducts async thunk is dispatched.

How to navigate to the update/detail page after a POST in RTK query

I've got a form with a POST in REACT RTK-query and then it should navigate to the second step, but for that I need to know the id of the newly created record. The id is not sent in POST but AutoIncremented in the backend. I don't want to navigate to the list-view to get that id but directly.
const [addMyModel] = useNewMyModelMutation();
const handleSubmit = (e: { preventDefault: () => void; }) => {
e.preventDefault();
const my_model = {
"user_id": user_id,
"date_time_field": new Date(),
"icm": icmChoice,
...
};
dispatch(setUniqueFilters(unique_filters))
//localStorage.setItem('unique_filters', JSON.stringify(unique_filters))
//localStorage.setItem('selection_positions', JSON.stringify(positions))
//localStorage.setItem('my_model', JSON.stringify(my_model))
addMyModel(my_model)
push(`/icm/mymodels/list`) // push(`/icm/mymodels/${answer_question}`);
What could be the best solution?
try to send the id in response from the backend after POST (Django Rest Framework),
change the primary key in the backend as such that I will know the id before handleSubmit (did that before but forgot the reason why I set everything back.)
saving step1 in the localStorage (but then there will be problems with updating previous records),
saving step1 in the state (but than I cannot refresh)
It's simple, but only in case your API returns a newly created item on POST response (which is expected for RESTfull API).
If so - just await the answer from hook:
const data = await addMyModel(my_model).unwrap()
// id should be in `data`
Considering that mutation "trigger" function returns a Promise, you can use it as usual:
addMyModel(my_model).then(newItem => {
if ("data" in newItem) {
const expectedId = item.data.id;
// use you expectedId
}
});
const [addMyModel, result] = useNewMyModelMutation();
...
const handleSubmit = (e: any) => {
e.preventDefault();
const my_model = {
"user_id": user_id,
"date_time_field": new Date(),
"icm": icmChoice,
...
};
addMyModel(my_model)}
result.status === "fulfilled" && push(`/icm/mymodels/update/step2/${result.data["id"]}`)

react-query always return stale data and no call is made to server

I recently started using react-query and have encountered the issue that always stale data is returned and no call to server is made. here is the react query related code:
export function useGetAccount(id: number){
return useQuery([`account${id}`, id], async (args) => {
const [key, accountId] = args.queryKey
const [acc, teams, modules] = await Promise.all([
getAccount(),
getTeams(),
getModules()])
let account: AccountDetail = {
accountId: acc.accountId,
userId: acc.userId,
companyId: acc.companyId,
login: acc.login,
email: acc.email,
description: acc.description,
isActive: acc.isActive,
providers: acc.providers,
teams: teams,
modules: modules
}
return account
async function getAccount() {
const api = createApi() // <= axios wrapper
const { data } = await api.get(`accounts/${accountId}`, undefined, undefined)
return data as AccountModel
}
async function getTeams() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/teams`, undefined, undefined)
const { collection } = data as ResponseCollectionType<AccountTeam>
return collection
}
async function getModules() {
const api = createApi()
const { data } = await api.get(`accounts/${accountId}/resources`, undefined, undefined)
const { collection } = data as ResponseCollectionType<ModuleAccessModel>
return collection
}
})
}
I even reduced the cache time but still to no avail. I do not see any calls made to server side except after a long delay or if I open the browser in incognito mode then first time the data is fetched and then no call is made.
this is used in a component which shows the details and is passed the id as a prop. everything is working fine except that the data is the one which was retrieved first time and even a refresh (F5) returns the stale data.
what changes do I need to make in this case?
[observation]: Ok, it does make a call but only after exact 5 minutes.
well the problem is not in react-query but in axios, described here Using JavaScript Axios/Fetch. Can you disable browser cache?
I used the same solution i.e. appending timestamp to the requests made by axios and everything worked fine.

How to setup a function which gets app settings and sets it as localStorage before the page loads. (next.js)

I've been working on a Next.JS web application for the past couple of days but I've reached a problem. The app has an API call (/api/settings) which returns some settings about the application from the database. Currently, I have a function which returns these settings and access to the first component:
App.getInitialProps = async () => {
const settingsRequest = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/settings`
);
const settingsResponse = await settingsRequest.json();
return { settings: settingsResponse };
};
This does work and I am able to pass in settings to components but there are two problems with this:
I need to nest the prop through many components to reach the components that I need
This request runs every time a page is reloaded/changed
Essentially, I need to create a system that does this:
runs a function in the _app.tsx getInitialProps to check if the data is already in localStorage, if not make the API request and update localStorage
have the localStorage value accessible from a custom hook.
Right now the problem with this is that I do not have access to localStorage from the app.tsx getInitialProps. So if anyone has an alternative to run this function before any of the page loads, please let me know.
Thanks!
I found a solution, it might be a janky solution but I managed to get it working and it might be useful for people trying to achieve something similar:
First we need to create a "manager" for the settings:
export const checkIfSettingsArePresent = () => {
const settings = localStorage.getItem("app_settings");
if (settings) return true;
return false;
};
export const getDataAndUpdateLocalStorage = async () => {
const r = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/settings`);
const response = await r.json();
localStorage.setItem("app_settings", JSON.stringify(response));
};
With that created we can add a UseEffect hook combined with a useState hook that runs our function.
const [doneFirst, setDoneFirst] = useState<boolean>(false);
useEffect(() => {
const settingsPreset = checkIfSettingsArePresent();
if (performance.navigation.type != 1)
if (settingsPreset) return setDoneFirst(true);
const getData = async () => {
await getDataAndUpdateLocalStorage();
setDoneFirst(true);
};
getData();
}, []);
//any other logic
if (!doneFirst) {
return null;
}
The final if statement makes sure to not run anything else before the function.
Now, whenever you hot-reload the page, you will see that the localStorage app_settings is updated/created with the values from the API.
However, to access this more simply from other parts of the app, I created a hook:
import { SettingsType } from "#sharex-server/common";
export default function useSettings() {
const settings = localStorage.getItem("app_settings") || {
name: "ShareX Media Server",
};
//#ts-ignore
return JSON.parse(settings) as SettingsType;
}
Now I can import useSettings from any function and have access to my settings.

In React, fetch data conditional on results of an initial fetch

We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}

Resources