Redux Toolkit Query: Reduce state from "mutation" response - redux-toolkit

Let's say I have an RESTish API to manage "posts".
GET /posts returns all posts
PATCH /posts:id updates a post and responds with new record data
I can implement this using RTK query via something like this:
const TAG_TYPE = 'POST';
// Define a service using a base URL and expected endpoints
export const postsApi = createApi({
reducerPath: 'postsApi',
tagTypes: [TAG_TYPE],
baseQuery,
endpoints: (builder) => ({
getPosts: builder.query<Form[], string>({
query: () => `/posts`,
providesTags: (result) =>
[
{ type: TAG_TYPE, id: 'LIST' },
],
}),
updatePost: builder.mutation<any, { formId: string; formData: any }>({
// note: an optional `queryFn` may be used in place of `query`
query: (data) => ({
url: `/post/${data.formId}`,
method: 'PATCH',
body: data.formData,
}),
// this causes a full re-query.
// Would be more efficient to update state based on resp.body
invalidatesTags: [{ type: TAG_TYPE, id: 'LIST' }],
}),
}),
});
When updatePost runs, it invalidates the LIST tag which causes getPosts to run again.
However, since the PATCH operation responds with the new data itself, I would like to avoid making an additional server request and instead just update my reducer state for that specific record with the content of response.body.
Seems like a common use case, but I'm struggling to find any documentation on doing something like this.

You can apply the mechanism described in optimistic updates, just a little bit later:
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query'
import { Post } from './types'
const api = createApi({
// ...
endpoints: (build) => ({
// ...
updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
// ...
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const { data } = await queryFulfilled
dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, data)
})
)
},
}),
}),
})

Related

How to differentiate prefetch requests from normal fetch requests?

I am having trouble differentiating regular query requests from prefetch query requests in RTK Query. My goal is simple; I want a global loading spinner whenever a query fetches data. However, I also want to implement prefetch in a List so that paginating through different pages feels instantaneous for the end-user. What is happening now is that when I go to the next page in my List it is correctly prefetched so switching happens instantly. But then my global loading spinner is triggered for the prefetching of the page after that (which I obviously don't want happening). So I want to find out how to differentiate the prefetching requests from the regular fetching requests.
I have done extensive searching both on SO and the issue-tracker of redux-toolkit, but without success. Also, I have looked into the query requests that are made from prefetch requests and regular requests but those seem identical (which I would understand since the rtk-query team probably abstracted this).
Relevant code below:
LoadingWrapper.tsx
const LoadingWrapper = ({ children }) => {
// HOW TO DIFFERENTIATE HERE BETWEEN QUERIES?
const isSomeQueryPending = useSelector((state: RootState) => Object.values(state.api.queries).some((query) => query.status === 'pending'));
return (
<>
<LoadingScreen loading={isSomeQueryPending} />
{children}
</>
);
};
EntityList.tsx
import React, {
useCallback, useEffect, useState,
} from 'react';
import { useGetEntitiesQuery, usePrefetch } from '../../../Path/To/My/Api';
const DEFAULT_PAGE_SIZE = 50;
const EntityList: (): ReactElement => {
const [filter, setFilter] = useState<IEntityFilter>({
search: '',
offset: 0,
limit: DEFAULT_PAGE_SIZE,
});
const { data } = useGetEntitiesQuery(filter);
const prefetchPage = usePrefetch('getEntities');
const prefetchNext = useCallback(() => {
const prefetchFilter = { ...filter, offset: filter.offset + filter.limit };
prefetchPage(prefetchFilter);
}, [prefetchPage, filter.offset]);
useEffect(() => {
if (!(filter.offset + filter.limit >= data?.numberOfEntities)) {
prefetchNext();
}
}, [data, prefetchNext, filter.offset]);
... // Some data handling and showing of data in a list unrelated.
}
Api.ts
// This is my (injected) API endpoint
getEntities: builder.query<IEntities, IEntityFilter>({
query: (filter) => ({ url: 'entities', params: filter }),
transformResponse: (baseQueryReturnValue: IEntitiesResponse) => baseQueryReturnValue.body,
providesTags: (result) => (result
? [
...result.entities.map(({ object_id }) => ({ type: 'Entity', id: object_id } as const)),
{ type: 'Entity', id: 'LIST' },
]
: [{ type: 'Entity', id: 'LIST' }]
),
}),
So the question is as follows: how can I differentiate between the normal fetch query (useGetEntitiesQuery) and the prefetched version of that in my LoadingWrapper.tsx. And if this is not possible, what is the recommended way of achieving my goal?

ReduxToolKit: correct way to use SelectFromResult options in a Query hook?

I am trying to understand how to correctly use SelectFromResult from the official documentation:
https://redux-toolkit.js.org/rtk-query/usage/queries#selecting-data-from-a-query-result
I have extended the Pokemon example to retrieve a filtered list of Pokemons ending in "saur" using SelectFromResult but the output results in a loss of error and isLoading data
live sandbox here:
https://codesandbox.io/s/rtk-query-selectfromresult-vvb7l
relevant code here:
the endpoint extracts out the relevant data with a transformResponse:
getAllPokemon: builder.query({
query: () => `pokemon/`,
transformResponse: (response: any) => {
console.log("transformResponse", response);
return response.results;
}
})
and the hook fails if i try to selectFromResult and I lose error and isLoading variables as they are no longer returned from the hook. If I comment out the SelectFromResult option they are then correctly returned.
export const PokemonList = () => {
const { data, error, isLoading } = useGetAllPokemonQuery(undefined, {
selectFromResult: ({ data }) => ({
data: data?.filter((item: Pokemon) => item.name.endsWith("saur"))
})
});
useEffect(() => {
if (data) console.log("filtered result", data);
}, [data]);
return (
<div>
{data?.map((item: Pokemon) => (
<p>{item.name}</p>
))}
</div>
);
};
My question: I dont want to lose fetch status when trying to filter results using the recommended method. How do I modify the above code to correctly SelectFromResult and maintain the correct error, isLoading, etc status values from the hook?
Solution found:
I passed in and returned the additional required variables (and tested by adding a polling interval to allow me to disconnect to force and error)
const { data, error, isLoading } = useGetAllPokemonQuery(undefined, {
selectFromResult: ({ data, error, isLoading }) => ({
data: data?.filter((item: Pokemon) => item.name.endsWith("saur")),
error,
isLoading
}),
pollingInterval: 3000,
});

Asynchronous Issues with JEST and MongoDB

I am getting inconsistent results with JEST when I try to remove items from a MongoDB Collection using the beforeEach() Hook.
My Mongoose schema and model defined as:
// Define Mongoose wafer sort schema
const waferSchema = new mongoose.Schema({
productType: {
type: String,
required: true,
enum: ['A', 'B'],
},
updated: {
type: Date,
default: Date.now,
index: true,
},
waferId: {
type: String,
required: true,
trim: true,
minlength: 7,
},
sublotId: {
type: String,
required: true,
trim: true,
minlength: 7,
},
}
// Define unique key for the schema
const Wafer = mongoose.model('Wafer', waferSchema);
module.exports.Wafer = Wafer;
My JEST tests:
describe('API: /WT', () => {
// Happy Path for Posting Object
let wtEntry = {};
beforeEach(async () => {
wtEntry = {
productType: 'A',
waferId: 'A01A001.3',
sublotId: 'A01A001.1',
};
await Wafer.deleteMany({});
// I also tried to pass in done and then call done() after the delete
});
describe('GET /:id', () => {
it('Return Wafer Sort Entry with specified ID', async () => {
// Create a new wafer Entry and Save it to the DB
const wafer = new Wafer(wtEntry);
await wafer.save();
const res = await request(apiServer).get(`/WT/${wafer.id}`);
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('productType', 'A');
expect(res.body).toHaveProperty('waferId', 'A01A001.3');
expect(res.body).toHaveProperty('sublotId', 'A01A001.1');
});
}
So the error I always get is related to duplicate keys when I run my tests more than once:
MongoError: E11000 duplicate key error collection: promis_tests.promiswts index: waferId_1_sublotId_1 dup key: { : "A01A001.3", : "A01A001.1" }
But I do not understand how I can get this duplicate key error if the beforeEach() were firing properly. Am I trying to clear the collection improperly? I've tried passing in a done element to the before each callback and invoking it after delete command. I've also tried implementing the delete in beforeAll(), afterEach(), and afterAll() but still get inconsistent results. I'm pretty stumped on this one. I might just removed the schema key all together but I would like to understand what is going on here with the beforeEach(). Thanks in advance for any advice.
It might be because you are not actually using the promise API that mongoose has to offer. By default, mongooses functions like deleteMany() do not return a promise. You will have to call .exec() at the end of the function chain to return a promise e.g. await collection.deleteMany({}).exec(). So you are running into a race condition. deleteMany() also accepts a callback, so you could always wrap it in a promise. I would do something like this:
describe('API: /WT', () => {
// Happy Path for Posting Object
const wtEntry = {
productType: 'A',
waferId: 'A01A001.3',
sublotId: 'A01A001.1',
};
beforeEach(async () => {
await Wafer.deleteMany({}).exec();
});
describe('GET /:id', () => {
it('Return Wafer Sort Entry with specified ID', async () => {
expect.assertions(4);
// Create a new wafer Entry and Save it to the DB
const wafer = await Wafer.create(wtEntry);
const res = await request(apiServer).get(`/WT/${wafer.id}`);
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('productType', 'A');
expect(res.body).toHaveProperty('waferId', 'A01A001.3');
expect(res.body).toHaveProperty('sublotId', 'A01A001.1');
});
}
Also, always expect the assertions with asynchronous code
https://jestjs.io/docs/en/asynchronous.html
You can read more about mongoose promises and query objects here
https://mongoosejs.com/docs/promises.html
Without deleting the schema index this seems to be the most reliable solution. Not 100% sure why it works over async await Wafer.deleteMany({});
beforeEach((done) => {
wtEntry = {
productType: 'A',
waferId: 'A01A001.3',
sublotId: 'A01A001.1',
};
mongoose.connection.collections.promiswts.drop(() => {
// Run the next test!
done();
});
});

Unable to get Moxios stubRequest to work

I'm having issues getting stubRequest to work properly. Here's my code:
it('should stub my request', (done) => {
moxios.stubRequest('/authenticate', {
status: 200
})
//here a call to /authenticate is being made
SessionService.login('foo', 'bar')
moxios.wait(() => {
expect(something).toHaveHappened()
done()
})
})
This works fine:
it('should stub my request', (done) => {
SessionService.login('foo', 'bar')
moxios.wait(async () => {
let request = moxios.requests.mostRecent()
await request.respondWith({
status: 200
})
expect(something).toHaveHappened()
done()
})
})
The second method just get's the last call though, and I'd really like to be able to explicitely stub certain requests.
I'm running Jest with Vue.
I landed here with a similar goal and eventually solved it using a different approach that may be helpful to others:
moxios.requests has a method .get() (source code) that lets you grab a specific request from moxios.requests based on the url. This way, if you have multiple requests, your tests don't require the requests to occur in a specific order to work.
Here's what it looks like:
moxios.wait(() => {
// Grab a specific API request based on the URL
const request = moxios.requests.get('get', 'endpoint/to/stub');
// Stub the response with whatever you would like
request.respondWith(yourStubbedResponseHere)
.then(() => {
// Your assertions go here
done();
});
});
NOTE:
The name of the method .get() is a bit misleading. It can handle different types of HTTP requests. The type is passed as the first parameter like: moxios.requests.get(requestType, url)
it would be nice if you show us the service. Service call must be inside the moxios wait func and outside must be the axios call alone. I have pasted a simplified with stubRequest
describe('Fetch a product action', () => {
let onFulfilled;
let onRejected;
beforeEach(() => {
moxios.install();
store = mockStore({});
onFulfilled = sinon.spy();
onRejected = sinon.spy();
});
afterEach(() => {
moxios.uninstall();
});
it('can fetch the product successfully', done => {
const API_URL = `http://localhost:3000/products/`;
moxios.stubRequest(API_URL, {
status: 200,
response: mockDataSingleProduct
});
axios.get(API_URL, mockDataSingleProduct).then(onFulfilled);
const expectedActions = [
{
type: ACTION.FETCH_PRODUCT,
payload: mockDataSingleProduct
}
];
moxios.wait(function() {
const response = onFulfilled.getCall(0).args[0];
expect(onFulfilled.calledOnce).toBe(true);
expect(response.status).toBe(200);
expect(response.data).toEqual(mockDataSingleProduct);
return store.dispatch(fetchProduct(mockDataSingleProduct.id))
.then(() => {
var actions = store.getActions();
expect(actions.length).toBe(1);
expect(actions[0].type).toBe(ACTION.FETCH_PRODUCT);
expect(actions[0].payload).not.toBe(null || undefined);
expect(actions[0].payload).toEqual(mockDataSingleProduct);
expect(actions).toEqual(expectedActions);
done();
});
});
});
})

Correct way to seed MongoDB with references via mongoose

I have three schemas, one which references two others:
userSchema
{ name: String }
postSchema
{ content: String }
commentSchema
{
content: String,
user: { ObjectID, ref: 'User' },
post: { ObjectID, ref: 'Post' }
}
How can I seed this database in a sane, scalable way? Even using bluebird promises it quickly becomes a nightmare to write.
My attempt so far involves multiple nested promises and is very hard to maintain:
User
.create([{ name: 'alice' }])
.then(() => {
return Post.create([{ content: 'foo' }])
})
.then(() => {
User.find().then(users => {
Post.find().then(posts => {
// `users` isn't even *available* here!
Comment.create({ content: 'bar', user: users[0], post: posts[0] })
})
})
})
This is clearly not the correct way of doing this. What am I missing?
Not sure about bluebird, but the nodejs Promise.all should do the job:
Promise.all([
User.create([{ name: 'alice' }]),
Post.create([{ content: 'foo' }])
]).then(([users, posts]) => {
const comments = [
{ content: 'bar', user: users[0], post: posts[0] }
];
return Comment.create(comments);
})
If you want to seed database with automatically references, use Seedgoose.
This is the easiest seeder for you to use. You don't need to write any program files, but only data files. And Seedgoose handles smart references for you. And by the way, I'm the author and maintainer of this package.
Try this it will work fine:
Note: Node Promise.all will make sure that the both query is executed properly and then return the result in Array:[Users, Posts],
If you get any error during execution of any query, it will be handle by catch block of the Promise.all.
let queryArray = [];
queryArray.push(User.create([{ name: 'alice' }]));
queryArray.push(Post.create([{ content: 'foo' }]));
Promise.all(queryArray).then(([Users, Posts]) => {
const comments = [
{ content: 'bar', user: Users[0], post: posts[0] }
];
return Comment.create(comments);
}).catch(Error => {
console.log("Error: ", Error);
})