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

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.

Related

How to make React-Query return cached/previous data if network is not connected/available?

I've modifying some existing React Native code where I've to check if network connection is available or not. If it is available, I've to fetch new data from API, otherwise I've to use cached data. This is what I've achieved so far:
export const useStudentData = (
studentId: Student['id']
): UseQueryResult<Student, Error> => {
const queryKey = studentDataKeys.list({ studentIdEq: studentData?.id });
const queryClient = useQueryClient();
const {isConnected} = useNetInfo();
if(!isConnected){
// return previously cached data here
}
const data = useQuery<Student, Error>(
studentDataKeys.detail(studentId),
async () => {
const { data: student } = await StudentDataAPI.fetchDataById(studentId);
return StudentData.deserialize({
...studentData,
assignments: studentData.assignments?.filter((assignment) => assignment.submitted)
});
},
{
staleTime: 1000 * 60 * 60 * 10,
retry: 0,
initialData: () => {
const previousStudentData = queryClient.getQueryData<Student[]>(queryKey);
return previousStudentData?.find((studentData) => studentData.id === studentId);
},
initialDataUpdatedAt: queryClient.getQueryState(queryKey)?.dataUpdatedAt,
onError() {
console.log("Error with API fetching");
}
}
);
return data;
};
How can I modify it so that if network connection is present, it should download new data otherwise return previous/old data that was cached in previous successful call?
Instead of reinventing the wheel, you can use the existing solution. That is:
import NetInfo from '#react-native-community/netinfo'
import { onlineManager } from '#tanstack/react-query'
onlineManager.setEventListener(setOnline => {
return NetInfo.addEventListener(state => {
setOnline(!!state.isConnected)
})
})
After implementing this, react-query will automatically refetch your data when the device is back online.
You're trying to implement a feature that React-Query provides to you for free.
React-Query will keep displaying old data until new data is available. By default, React-Query will stop trying to fetch data entirely until you are back online.
Additionally, you can set the refetchOnReconnect flag to true (which is also the default) in order to request fresh data the moment you are online.
You can rely on the queryKey for that. In detail, react-query caches the result based on the key.
Your current key is studentDataKeys.detail(studentId), what about replacing it with [studentDataKeys.detail(studentId), isConnected]?

MobX not hydrating in next.js state when fetching async data

I have a MobX store where I have a function doing an API call. It works fine it's getting the data but it doesn't update the already rendered page. I'm following this tutorial https://medium.com/#borisdedejski/next-js-mobx-and-typescript-boilerplate-for-beginners-9e28ac190f7d
My store looks like this
const isServer = typeof window === "undefined";
enableStaticRendering(isServer);
interface SerializedStore {
PageTitle: string;
content: string;
isOpen: boolean;
companiesDto: CompanyDto[],
companyCats: string[]
};
export class AwardStore {
PageTitle: string = 'Client Experience Awards';
companiesDto : CompanyDto[] = [];
companyCats: string[] = [];
loadingInitial: boolean = true
constructor() {
makeAutoObservable(this)
}
hydrate(serializedStore: SerializedStore) {
this.PageTitle = serializedStore.PageTitle != null ? serializedStore.PageTitle : "Client Experience Awards";
this.companyCats = serializedStore.companyCats != null ? serializedStore.companyCats : [];
this.companiesDto = serializedStore.companiesDto != null ? serializedStore.companiesDto : [];
}
changeTitle = (newTitle: string) => {
this.PageTitle = newTitle;
}
loadCompanies = async () => {
this.setLoadingInitial(true);
axios.get<CompanyDto[]>('MyAPICall')
.then((response) => {
runInAction(() => {
this.companiesDto = response.data.sort((a, b) => a.name.localeCompare(b.name));
response.data.map((company : CompanyDto) => {
if (company.categories !== null ) {
company.categories?.forEach(cat => {
this.addNewCateogry(cat)
})
}
})
console.log(this.companyCats);
this.setLoadingInitial(false);
})
})
.catch(errors => {
this.setLoadingInitial(false);
console.log('There was an error getting the data: ' + errors);
})
}
addNewCateogry = (cat : string) => {
this.companyCats.push(cat);
}
setLoadingInitial = (state: boolean) => {
this.loadingInitial = state;
}
}
export async function fetchInitialStoreState() {
// You can do anything to fetch initial store state
return {};
}
I'm trying to call the loadcompanies from the _app.js file. It calls it and I can see in the console.log the companies etc but the state doesn't update and I don't get to see the actual result. Here's the _app.js
class MyApp extends App {
constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = {
awardStore: new AwardStore()
};
this.state.awardStore.loadCompanies();
}
// Fetching serialized(JSON) store state
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
const initialStoreState = await fetchInitialStoreState();
return {
...appProps,
initialStoreState
};
}
// Hydrate serialized state to store
static getDerivedStateFromProps(props, state) {
state.awardStore.hydrate(props.initialStoreState);
return state;
}
render() {
const { Component, pageProps } = this.props;
return (
<Provider awardStore={this.state.awardStore}>
<Component {...pageProps} />
</Provider>
);
}
}
export default MyApp;
In the console.log I can see that this.companyCat is update but nothing is changed in the browser. Any ideas how I can do this? Thank you!
When you do SSR you can't load data through the constructor of the store because:
It's does not handle async stuff, so you can't really wait until the data is loaded
Store is created both on the server side and on the client too, so if theoretically constructor could work with async then it still would not make sense to do it here because it would load data twice, and with SSR you generally want to avoid this kind of situations, you want to load data once and reuse data, that was fetched on the server, on the client.
With Next.js the flow is quite simple:
On the server you load all the data that is needed, in your case it's loaded on the App level, but maybe in the future you might want to have loader for each page to load data more granularly. Overall it does not change the flow though
Once the data is loaded (through getInitialProps method or any other Next.js data fetching methods), you hydrate your stores and render the application on the server side and send html to the client, that's SSR
On the client the app is initialized again, though this time you don't want to load the data, but use the data which server already fetched and used. This data is provided through props to your page component (or in this case App component). So you grab the data and just hydrate the store (in this case it's done with getDerivedStateFromProps).
Based on that, everything you want to fetch should happen inside getInitialProps. And you already have fetchInitialStoreState method for that, so all you need to do is remove data fetching from store constructor and move it to fetchInitialStoreState and only return the data from it. This data will then go to the hydrate method of your store.
I've made a quick reproduction of your code here:
The huge downside if App.getInitialProps is that it runs on every page navigation, which is probably not what you want to do. I've added console.log("api call") and you can see in the console that it is logged every time you navigate to any other page, so the api will be called every time too, but you already have the data so it's kinda useless. So I recommend in the future to use more granular way of loading data, for example with Next.js getServerSideProps function instead (docs).
But the general flow won't change much anyway!
Calling awardStore.loadCompanies in the constructor of MyApp is problematic because the loadCompanies method is populating the store class. What you want is to hydrate the store with the companyCats data. Since server and client stores are distinct, you want to load the data you need on the server side i.e. fetchInitialStoreState (or load it from a page's getStaticProps/getServerSideProps method) so that you can pass it into the hydrate store method from page/app props.
Note loadCompanies is async so it'll be [] when getDerivedStateFromProps is called so there's nothing to hydrate. For your existing hydrate method to work you need initialStoreState to be something like the fetchInitialStoreState method below. Alternatively if it's fetched on the page level, the hydrate may be closer to initialData?.pageProps?.companyCats
It's common to see the store hydration as needed for each page though it's still valid to call loadCompanies() from the client side. There's a lot I didn't get a chance to touch on but hopefully this was somewhat helpful.
export const fetchInitialStoreState = async() => {
let companyCats = [];
try {
const response = await axios.get < CompanyDto[] > ('MyAPICall')
response.data.map((company: CompanyDto) => {
if (Array.isArray(company.categories) && company.categories.length > 0) {
companyCats.push(...company.categories)
}
})
} catch (error) {
// Uh oh...
}
return {
serializedStore: {
companyCats,
// PageTitle/etc
}
}
}

How to fetch with parameters using React Query?

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.

React Recoil: State not being saved across navigation to different URLs within App

I'm getting started with Recoil for a React App, but running into some issues, or at least some behavior I'm not expecting.
I'd like to be able to use one component to render many different "views" based on the URL. I have a useEffect in this component that switches based on the location.pathname and based on that pathname, it'll make an API call. But before it makes the API call, it checks the length of the atom to see if it's empty or not, then will call the API and set the atom based on the API call.
However, when I navigate to a different URL and come back to one I've already visited, the API is called again, even though I've previously set the state for that URL.
The behavior I'm expecting is that once a URL has been visited and the return from the API is stored in an Atom, the API call isn't made again when leaving the URL and coming back.
Relevant code below:
Atom.js
export const reports = atom({ key: "reports", default: { country: [], network: [], }, });
the one component that will render different data based on the reports atom.
import { useRecoilState } from "recoil";
import { reports } from "../globalState/atom";
const TableView = ({ columns, }) => {
const location = useLocation();
const [report, setReport] = useRecoilState(reports);
const currentView = location.pathname.split("/")[1];
useEffect(() => {
const getReportsData = async () => {
switch (location.pathname) {
case "/network":
if (report[currentView].length === 0) {
const response = await fetch("/api");
const body = await response.json();
setReport(
Object.assign({}, report, {
[currentView]: body,
})
);
console.log('ran')
break;
}
getReportsData();
}, [])
As previously mentioned, that console.log is ran every time I navigate to /network, even if I've already visited that URL.
I've also tried doing this with selectors.
Atom.js
export const networkState = atom({
key: "networkState",
default: networkSelector,
});
export const networkSelector = selector({
key: "networkSelector",
get: async ({ get }) => {
try {
const body = await fetch("/api/network").then((r) => r.json());
return body;
} catch (error) {
console.log(error);
return [];
}
}
Component
import {useRecoilStateLoadable} from "recoil"
import {networkState} from "../globalState/atom";
const Table = ({columns}) => {
const [networkData, setNetworkData] =
useRecoilStateLoadable(networkState);
And then a switch statement based on networkData.state
}
Any help would be greatly appreciated, thank you!

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.

Resources