Callback after success and data changed - react-query

I used the onSuccess callback, but it is called every time the data is successfully retrieved.
I expect to be called only when the data changed(deep equal). Is there any good way to do this?
useQuery(..., {
onSuccess(data) {
console.log(data)
}
})
// first: [1], second: [2], // => call
// first: [1], second: [1], // => not call
How to do this, thanks a lot!

onSuccess is tied to successful data fetching. If you want to spawn a side-effect whenever data changes, use a useEffect:
const { data } = useQuery(...)
useEffect(() => ..., [data])

Related

react-query: How to process a queue, one item at a time, and remove the original data after processing?

I'm using react-query 4 to get some data from my server via JSON:API and create some objects:
export type QueryReturnQueue = QueueObject[] | false;
const getQueryQueue = async (query: string): Promise<QueryReturnQueue> => {
const data = await fetchAuth(query);
const returnData = [] as QueueObject[];
if (data) {
data.map((queueItem) => returnData.push(new QueueObject(queueItem)));
return returnData;
}
return false;
};
function useMyQueue(
queueType: QueueType,
): UseQueryResult<QueryReturnQueue, Error> {
const queryKey = ['getQueue', queueType];
return useQuery<QueryReturnQueue, Error>(
queryKey,
async () => {
const query = getUrl(queueType);
return getQueryQueue(query);
},
);
}
Then I have a component that displays the objects one at a time and the user is asked to make a choice (for example, "swipe left" or "swipe right"). This queue only goes in one direction-- the user sees a queueObject, processes the object, and then goes to the next one. The user cannot go back to a previous object, and the user cannot skip ahead.
So far, I've been using useContext() to track the index in the queue as state. However, I've been running into several bugs with this when the queue gets refreshed, which happens a lot, so I thought it would be easier to directly manipulate the data returned by useQuery().
How can I remove items as they are processed from the locally cached query results?
My current flow:
Fetch the queue data and generation objects with useQuery().
Display the queue objects one at a time using useContext().
Mutate the displayed object with useMutation() to modify useContext() and then show the next object in the cached data from useQuery().
My desired flow:
Fetch the queue data and generation objects with useQuery().
Mutate the displayed object with useMutation(), somehow removing the mutated item from the cached data from useQuery() (like what shift() does for arrays).
Sources I consulted
Best practices for editing data after useQuery call (couldn't find an answer relevant to my case)
Optimistic updates (don't know how to apply it to my case)
My desired flow:
Fetch the queue data and generation objects with useQuery().
Mutate the displayed object with useMutation(), somehow removing the mutated item from the cached data from useQuery() (like what shift() does for arrays).
This is the correct way to think about the data flow. But mutations shouldn't be updating the cache with data, they should be invalidating existing cache data.
You have defined your query correctly. Now you simply have to instruct your mutation function (which should be making an API call that updates the records queue) to invalidate all existing queries for the data in the onSuccess handler.
e.g.
function useMyMutation(recordId, queueType) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({id, swipeDirection}) =>
asyncAPICall(`/swipes/${id}`, { swipeDirection }),
onSuccess: () => queryClient.invalidateQueries(['getQueue', queueType]);
});
}
As suggested by #Jakub Kotrs:
shift the first item from the list + only ever display the first
I was able to implement this in my useMutation() hook:
onMutate: async (queueObjectRemoved) => {
const queryKey = ['getQueue', queueType];
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update).
await queryClient.cancelQueries({
queryKey,
});
if (data?.[0]?.id === queueObjectRemvoed.data.id) {
// Optimistically update the data by removing the first item.
data.shift();
queryClient.setQueryData(queryKey, () => data);
} else {
throw new Error('Unable to set queue!');
}
},
onError: () => {
const queryKey = ['getQueue', queueType];
setShowErrorToast(true);
queryClient.invalidateQueries(
queryKey,
);
},
This way users can process all the items in the current queue before needing to refetch.

How to use Observables as a lazy data source

I'm wrapping an API that emits events in Observables and currently my datasource code looks something like this, with db.getEventEmitter() returning an EventEmitter.
const Datasource = {
getSomeData() {
return Observable.fromEvent(db.getEventEmitter(), 'value');
}
};
However, to actually use this, I need to both memoize the function and have it return a ReplaySubject, otherwise each subsequent call to getSomeData() would reinitialize the entire sequence and recreate more event emitters or not have any data until the next update, which is undesirable, so my code looks a lot more like this for every function
const someDataCache = null;
const Datasource = {
getSomeData() {
if (someDataCache) { return someDataCache; }
const subject = new ReplaySubject(1);
Observable.fromEvent(db.getEventEmitter(), 'value').subscribe(subject);
someDataCache = subject;
return subject;
}
};
which ends up being quite a lot of boilerplate for just one single function, and becomes more of an issue when there are more parameters.
Is there a better/more elegant design pattern to accomplish this? Basically, I'd like that
Only one event emitter is created.
Callers who call the datasource later get the most recent result.
The event emitters are created when they're needed.
but right now I feel like this pattern is fighting the Observable pattern, resulting a bunch of boilerplate.
As a followup to this question, I ended up commonizing the logic to leverage Observables in this way. publishReplay as cartant mentioned does get me most of the way to what I needed. I've documented what I've learned in this post, with the following tl;dr code:
let first = true
Rx.Observable.create(
observer => {
const callback = data => {
first = false
observer.next(data)
}
const event = first ? 'value' : 'child_changed'
db.ref(path).on(event, callback, error => observer.error(error))
return {event, callback}
},
(handler, {event, callback}) => {
db.ref(path).off(event, callback)
},
)
.map(snapshot => snapshot.val())
.publishReplay(1)
.refCount()

#ngrx Effect does not run the second time

I've just started learning about #ngrx/store and #ngrx.effects and have created my first effect in my Angular/Ionic app. It runs ok the first time but if I dispatch the event to the store again (i.e when clicking the button again), nothing happens (no network call is made, nothing in console logs). Is there something obvious I'm doing wrong? Here's the effect:
#Effect() event_response$ = this.action$
.ofType(SEND_EVENT_RESPONSE_ACTION)
.map(toPayload)
.switchMap((payload) => this.myService.eventResponse(payload.eventId,payload.response))
.map(data => new SentEventResponseAction(data))
.catch((error) => Observable.of(new ErrorOccurredAction(error)));
Thanks
It sounds like an error is occurring. In that situation, the action in the observable returned by catch will be emitted into the effect's stream and the effect will then complete - which will prevent the effect from running after the error action is emitted.
Move the map and the catch into the switchMap:
#Effect() event_response$ = this.action$
.ofType(SEND_EVENT_RESPONSE_ACTION)
.map(toPayload)
.switchMap((payload) => this.myService
.eventResponse(payload.eventId, payload.response)
.map(data => new SentEventResponseAction(data))
.catch((error) => Observable.of(new ErrorOccurredAction(error)))
);
Composing the catch within the switchMap will prevent the effect from completing if an error occurs.
You must move map() and catchError() into swithchMap() as following
#Effect()
public event_response$ = this.action$.pipe(
ofType(SEND_EVENT_RESPONSE_ACTION),
switchMap((payload) => {
return this.myService.eventResponse(payload.eventId,payload.response).pipe(
map((data: DataType) => new SentEventResponseAction(data)),
catchError((error) => Observable.of(new ErrorOccurredAction(error)))
})
);
);
Please note that, evetResponse() method inside myService should return an observable in order to use pipe afterward.
In case your method inside service returns Promise, you can convert it into an observable by the use of from in the rxjs package as below:
import { from } from 'rxjs';
...
const promise = this.myService.eventResponse(payload.eventId,payload.response);
const observable = from(promise);
return observable.pipe(...
For more and detail description take a look at this link

Confused about side effects / ContinueAfter

I have a scenario in which I download parent entities from an api and save them to a database. I then want, once all of the parents have been saved, to download and save their children.
I've seen (or misunderstood) some comments about how this is a side-effect as I will not be passing the result of the parent save operation to the save children operation. I simply want to begin it when the parents are saved.
Could someone explain to me the best way of doing this?
Perhaps try something like this:
Observable
.Create<int>(o =>
{
var parentIds = new int?[] { null };
return
Observable
.While(
() => parentIds.Any(),
parentIds
.ToObservable()
.Select(parentId => Save(parentId)))
.Finally(() => { /* update `parentIds` here with next level */ })
.Subscribe(o);
})
.Subscribe(x => { });
This is effectively doing a breadth-first traversal of all of the entities, saving them as it goes, but outputting a single observable that you can subscribe to.

Reactive extensions(Rx) Switch() produces new observable which is not subscribed to provided OnCompleted()

I have a problem with my Rx subscription using Switch statement.
_performSearchSubject
.AsObservable()
.Select(_ => PerformQuery())
.Switch()
.ObserveOn(_synchronizationContextService.SynchronizationContext)
.Subscribe(DataArrivedForPositions, PositionQueryError, PositionQueryCompleted)
.DisposeWith(this);
The flow is:
Some properties change and the performSearchSubject.OnNext is called
The PerformPositionQuery() is called, which returns a observer each time it is hit
The service which responds through this observer calls OnNext twice and OnCompleted once when the data receive is done
Method DataArrivedForPositions is called twice as expected
Method PositionQueryCompleted is never called, though observer.OnCompleted() is called inside my data service.
Code for dataService is:
protected override void Request(Request request, IObserver<Response> observer)
{
query.Arrive += p => QueryReceive(request.RequestId, p, observer, query);
query.Error += (type, s, message) => QueryError(observer, message);
query.NoMoreData += id => QueryCompleted(observer);
query.Execute(request);
}
private void QueryError(IObserver<PositionSheetResponse> observer, string message)
{
observer.OnError(new Exception(message));
}
private void QueryCompleted(IObserver<PositionSheetResponse> observer)
{
observer.OnCompleted();
}
private void QueryReceive(Guid requestId, Qry0079Receive receiveData, IObserver<PositionSheetResponse> observer, IQry0079PositionSheet query)
{
observer.OnNext(ConvertToResponse(requestId, receiveData));
}
Switch result will only Complete when your outer observable (_performSearchSubject) completes. I assume in your case this one never does (it's probably bound to a user action performing the search).
What's unclear is when you expect PositionQueryCompleted to be called. If It's after each and every successful query is processed, then your stream needs to be modified, because Switch lost you the information that the query stream completed, but it also lacks information about the UI (wrong scheduler even) to say whether its data was actually processed.
There may be other ways to achieve it, but basically you want your query stream complete to survive through Switch (which currently ignore this event). For instance you can transform your query stream to have n+1 events, with one extra for the complete:
_performSearchSubject
.AsObservable()
.Select(_ =>
PerformQuery()
.Select(Data => new { Data, Complete = false})
.Concat(Observable.Return(new { Data = (string)null, Complete = true })))
You can safely apply .Switch().ObserveOn(_synchronizationContextService.SynchronizationContext) on it, but then you need to modify your subscription:
.Subscribe(data => {
if (data.Complete) DataArrivedForPositions(data.Data);
else PositionQueryCompleted()
}, PositionQueryError)