Suspense returns data too fast in #tanstack/react-query v4 - react-query

I am upgrading a React app from react-query v3 to #tanstack/react-query v4.
Almost everything works, but I'm having a problem with Suspense.
I have a react component:
const WrapperPageEdit: React.FC<MyProps> = ({
pageUuid,
redirect,
}: MyProps) => {
const FormPage = React.lazy(() => import('./FormPage'));
const { data } = usePageView(pageUuid);
if (data?.[0]) {
const pageObjectToEdit= data[0];
const content = pageObjectToEdit.myStuff.content;
return (
<Suspense
fallback={<Trans id="loading.editor">Loading the editor...</Trans>}
>
<FormPage
id={uuid}
content={content}
redirect={redirect}
/>
</Suspense>
);
}
return <p>No data.</p>;
};
And here's my query:
export function usePageView(
uuid: string,
): UseQueryResult<DrupalPage[], Error> {
return useQuery<DrupalPage[], Error>(
queryKeyUsePageView(uuid),
async () => {
return fetchAnon(getPageByPageUuid(uuid));
},
{
cacheTime: YEAR_MILLISECONDS,
staleTime: YEAR_MILLISECONDS,
onSuccess: (data) => {
if (data?.[0]) {
data.map((element) => processResult(element));
}
},
},
);
}
This works in v3 but fails in v4 with the following error:
TypeError: Cannot read properties of undefined (reading 'content')
The reason the property is undefined is because that property is set by the processing in onSuccess (data.map).
The issue appears to be that in v4, the component WrapperPageEdit is refreshed before onSuccess in the usePageView query has finished processing, whereas in v3, the component WrapperPageEdit is not refreshed until the onSuccess data.map is complete.
How can I correctly fix this? I can write some additional code to try to check whether the onSuccess data.map is complete, but since react-query handled this automatically in v3, I'd like to rewrite my code in v4 so that it is the same.

The problem is likely that you are mutating the data in onSuccess. Directly modifying data in callbacks is not a good idea. Instead, do your transformation for example directly in the queryFn:
async () => {
const data = fetchAnon(getPageByPageUuid(uuid));
if (data?.[0]) {
data.map((element) => processResult(element));
}
return data
},
other good places to do data transformation is e.g. the select option, but it should always happen in an immutable way, because otherwise, you are overwriting the cached data inadvertently. React prefers updates to be immutable.

Related

Redux toolkit serializableMiddleware doesn't work

I encountered with an issue of serializableMiddleware in redux toolkit. I need to store firestore timestamps inside my redux store and don't want that serializableMiddleware logged this text:
A non-serializable value was detected in the state, in the path: path to timestamp. Value: nt {seconds: > 1675422816, nanoseconds: 106000000}
Take a look at the reducer(s) handling this action.
So i take a look at the serializableMiddleware in redux toolkit docs and find out that there is an option isSerializable?: (value: any) => boolean. When I return true from this method, value should consider serializable, but still logs error out even if I always return true.
I wrote function but it didn't work:
const isSerializable = (value: any) =>
value instanceof Timestamp || isPlain(value);
const serializableMiddleware = createSerializableStateInvariantMiddleware({
isSerializable
})
Here is how I setup my store:
export const setupStore = () => {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(serializableMiddleware)
})
}
The problem here is that getDefaultMiddleware() already adds the serializable middleware as part of the default setup. You're creating a second instance of the middleware, but that doesn't change the first instance.
Instead, you need to customize the first instance by passing the right options to getDefaultMiddleware():
export const setupStore = () => {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
isSerializable: myCustomIsSerializableFunction
}
})
})
}

How do I define an action without having to implement a reducer for it?

I'm converting some existing redux code to the toolkit way. We have a lot of actions that trigger thunks (to load data from backend) but dont have a reducer. Our pattern being the load/success/fail triple. Basically only the success and fails need a reducer statement. How do I do this with the toolkit? Do I have to put in a reducer that just returns the unchanged state for the load actions?
With redux-toolkit you have a few options here...
1. Existing thunks + RTK actions
If you only need to update one slice of your store with the loaded data, you can create “success” and “fail” actions in the reducers property on that slice. Then, change your thunk to dispatch those instead of the old success/fail actions.
const slice = createSlice({
name: 'data',
initialState: {},
reducers: {
fetchDataSuccess(state, action) {
// Do something with the response
},
fetchDataError(state, action) {
// Do something with the error
}
}
}
const { fetchDataSuccess, fetchDataError } = slice.actions
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch(fetchDataSuccess(response.data)))
.catch(error => dispatch(fetchDataError(error))
}
export default slice.reducer
2. Existing thunks + extraReducers
If you don't want to refactor the existing thunk, or if the actions will be used across multiple slices, you can use the extraReducers property.
// These can also be defined in a separate file and imported
const FETCH_SUCCESS = 'data/FETCH_SUCCESS'
const FETCH_FAIL = 'data/FETCH_FAIL'
export function fetchData() {
return dispatch => api.getData()
.then(response => dispatch({ type: FETCH_SUCCESS, payload: response.data }))
.catch(error => dispatch({ type: FETCH_FAIL, payload: error }))
}
const slice = createSlice({
// ... the usual properties
extraReducers: {
[FETCH_SUCCESS](state, action) {
// Do something with the response
},
[FETCH_FAIL](state, action) {
// Do something with the error
}
}
}
3. createAsyncThunk
This approach is similar to the above, but the createAsyncThunk utility handles a lot of it for you, like catching errors, dispatching the actions at the right time, etc.
const fetchData = createAsyncThunk(
'data/fetchData',
() => api.getData().then(response => response.data)
)
const slice = createSlice({
// ... the usual properties
extraReducers: {
[fetchData.fulfilled](state, action) {
// Do something with the response
},
[fetchData.rejected](state, action) {
// Do something with action.error
}
}
}
// Components still call this like a normal function: fetchData()
export { fetchData }
export default slice.reducer
Whichever way you end up going, if you're not using the "load" action (or .pending from createAsyncThunk), you don't need to add it to either reducers or extraReducers.
I think you can simply create thunk-actions.ts (or eg. saga-actions.ts) file to keep actions that trigger data loading.
import { createAction } from '#reduxjs/toolkit';
export const fetchUserComments = createAction<{ id: string }>(
'fetchUserComments',
);
all actions that have reducer's logic will be generated by slice

Axios in Vuex Store returning promise and not data

I'm not sure why I can't get the data I want from this axios.get in my Vuex store.
I've setup this action to commit a change to my mutation like this:
mutations: {
updateShop: (state, payload ) => {
state.shop = payload;
return state.shop;
}
},
actions: {
getShop: ({ commit }) => {
return axios.get("/admin/wine_app/shops").then(response => {
debugger; // I DO have data here??
commit('updateShop', response.data);
});
}
}
But when I stop it with that debugger I DO have the data, but when I use the getShop action in an component I see the promise being returned.
Any idea why?
EDIT:
It MIGHT just not be ready!! I'm seeing this in the console
Promise {<pending>}
__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
make the action getShop async
getShop: async ({ commit }) => {
const response = await axios.get("/admin/wine_app/shops");
debugger; // I DO have data here??
commit('updateShop', response.data);
}
await the action where you call it
await this.$store.dispatch('getShop')
Use the shop state prop in your code
this.$store.state.shop
or use mapState if you would like to use several state props.
Also make sure not to return any data from mutations. They must change state and not return its props.
The reason you are seeing a promise is because your action is returning the axios call. Remove the return in both your mutation and action methods.
Your action method uses axios to retrieve the data. This then commits your mutation with the response data. Your mutation method updates the state with that data. At this point your state.shop is updated.
Within your Vue components you can access that data by accessing the state as a computed property.
computed: {
shop() {
return this.$store.state.shop
}
// Or use mapState
...mapState(['shop'])
}
Whenever your state changes this should update in your components due to reactivity.

Ionic view not updating after return from provider promise

I'm very new to Ionic and JS programming in general so please forgive my ignorance. I've been able to get data from other REST providers I've setup and have the updated values display fine. Pretty much copied the code from some other working functions. This time, no matter what I try, nothing will update.
Provider:
return new Promise(resolve => {
this.http.post(this.apiUrl)
.subscribe(res => {
resolve(res);
},
(err: HttpErrorResponse) => {
if (err.error instanceof Error) {
this.error = {"text":"App error occured."};
console.log('Client-side error occured.');
} else {
this.error = {"text":"Cloud server error occured."};
console.log('Cloud server error occured:'+err);
}
return this.error;
});
});
}
HTML:
<ion-item>
<ion-label stacked>Make</ion-label>
{{vesselData?.make}}
</ion-item>
Function:
vesselData = {"make":""};
updateVesselInfo() {
const data = JSON.parse(localStorage.getItem('userData'));
this.vesselProvider.getVesselData(data.userData.sim).then(vData => {
this.vesselData = vData;
}).catch(console.log.bind(console));
}, (err) => {
console.log("Vessel: ".err);
});
If I log the data returned from the provider in the .then(), it shows the provider returned the correct data. However, it's not updating any of the vesselData variables. Any idea where I'm going wrong here?
So modern way is to provide method in your provider that returns Observable and then in your component you just call this method and subscribe to it to obtain data:
In your provider:
getVesselData() {
return this.http.post(this.apiUrl)
.pipe(
catchError(this.yourErrorHandlerInsideProviderHere)
)
}
Now in your component:
vesselData = {"make":""};
updateVesselInfo() {
this.provider.getVesselData().subscribe( vesselData => {
this.vesselData = vesselData;
})
}
So ideal is to keep error handling inside provider here and within component your methods should be light weight.
This example should work for you as long as you are on Angular 4.3+ using modern HTTP module that comes with it.
Update:
Please ensure you properly bind to template. Here is example:
https://stackblitz.com/edit/ionic-wqrnl4
I skipped the rest call (http), but the principle is the same.

play framework - what is the proper way to return list of data?

I'm new to the reactive/observables concept.
I've defined a route for getting Products like this:
GET /products controllers.ProductController.findProducts
and the implementation in the controller:
def findProducts() = secured.async { implicit request =>
productDao.find()
.map(products => Ok(Json.toJson(products)))
// products: List[Product]
.recover(errHandler)
}
Then, on the client-side, I'm making the call and subscribing to it like this:
let sub = this.find()
.subscribe(
products => {
this.products = products;
this.productsBehaviorSubject.next( this.products );
if (!!sub)
sub.unsubscribe();
},
error => console.log("Error fetching units: " + error)
);
As soon as I get the data, I unsubscribe from it. (basically using it like Promises).
So I wonder, if this is the right way to do it. Instead of returning List[Product] in one single response, should I be returning in several responses??
def findProducts() = secured.async { implicit request =>
productDao.find()
.map(products => {
products.map(p => Ok(Json.toJson(p))) // haven't tried this yet
})
// products: List[Product]
.recover(errHandler)
}
then on the client side .. maybe .:
this.find()
.take( 50 )
.subscribe(
product => {
this.products.push( product )
},
error => console.log("Error fetching units: " + error),
() => this.productsBehaviorSubject.next( this.products )
);
Your first approach is correct, you will notice that the secured.async expects the code block to return Future[Result]. So if you try to map over the products then the signature will be Future[Seq[Result]] which will give you a compilation error.
Also in my experience, returning the full list in a single response is the standard way to code.