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

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,
});

Related

Redux Toolkit - do not send request when query param is invalid

I've checked the redux toolkit docs and don't see an example of this typical use case: do not send the request of the query has an invalid param.
For example, a get request to endpoint /categories/{name} requires a name value. If name does not have a value, then the request should not be made.
const baseQuery = fetchBaseQuery({
baseUrl: Constants.PATHWAY_API_URL
});
export const pathwayApi = createApi({
reducerPath: 'pathwayApi',
baseQuery: baseQueryWithReAuth,
endpoints: builder => ({
getSubCategories: builder.query({
// NETWORK REQUEST SHOULD NOT BE MADE IF "name" param is falsy
query: name => `${Constants.PATHWAY_API.CATEGORIES_PATH_NAME}/${name}`,
}),
}),
});
I want to add this type of param validation to all my queries that require a param value or values. What's the recommended approach / pattern for handling this validation at the createApi (or possibly fetchBaseQuery) layer?
Thanks in advance!
You can actually throw an error in your query function.
export const pathwayApi = createApi({
reducerPath: "pathwayApi",
baseQuery: baseQueryWithReAuth,
endpoints: (builder) => ({
getSubCategories: builder.query({
// NETWORK REQUEST SHOULD NOT BE MADE IF "name" param is falsy
query: (name) => {
if (!name) {
throw new Error("Category name is required.");
}
return `${Constants.PATHWAY_API.CATEGORIES_PATH_NAME}/${name}`;
}
})
})
});
When this happens, your hook will have isError: true but no network request will be made. The error property of your hook will be a SerializedError object with properties name, message and stack, which you can use to display the error in your UI.
This is the same type of error object that you get if you have a TypeError somewhere in your code. Note that JavaScript errors will have error.message while API errors (FetchBaseQueryError) will have error.error.
const Category = ({ name }) => {
const { data, error, isError } = useGetSubCategoriesQuery(name);
return (
<div>
<h3>Name: "{name}"</h3>
{isError && (
<div>{error?.error ?? error?.message}</div>
)}
</div>
);
};
CodeSandbox Link

Redux Toolkit Query: Reduce state from "mutation" response

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)
})
)
},
}),
}),
})

VuexFire Synchronization with bind not working

I'm currently using VuexFire to bind the Cloud Firestore to my Vuex State. I'm having some issues getting it to work, any help would be appreciated.
What I'm currently doing is the following:
Vue.js File:
methods:{
...mapActions("comments", ['bindArticleComments']),
},created(){
this.bindArticleComments()
},
actions/comments file
export const bindArticleComments = firestoreAction(({ bindFirestoreRef }) => {
return bindFirestoreRef('articleComments', collectionRef('comments'))
})
firebase services file
export const collectionRef = (collectionName) => {
return firestore().collection(collectionName)
}
What is strange about this is that I'm already doing the same procedure for a different Vuex state field. There it seems to be working without an issue. Is there anything that anyone thinks I might not be doing properly?
Strangely i got it working , although i'm struggling to understand how and why it's working.In my Vue js file i placed the this.bindArticleComments() after downloading the data and at creation.
methods:{
downloadComments(){
const { articlesCommentRef } = this.$fb
articlesCommentRef(this.loadedArticle.id).get()
.then(querySnapshot => {
this.setArticleComments(querySnapshot.docs.map((doc) => doc.data()))
this.bindArticleComments(this.loadedArticle.id)})
.catch(error => console.log("Error getting documents: ", error))
.finally(() => {this.$q.loading.hide()})
}
},
created(){
this.bindArticleComments(this.loadedArticle.id)
},
mounted(){
this.downloadComments()
}

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();
});
});
});
})

axios and mongo - trying to delete using the mapped _id as the perimeter. It responds with 200 but object remains in the db

Trying to make a simple CRUD app using react, axios and mongoose.
Here is my axios:
deleteUser(id) {
axios.delete(`${rootUrl}/api/users/${this.state.id}`, {
params: { id }
})
.then(response => {
// this.setState({ users: response.data });
console.log('deleteUser response', response, this.state);
});
}
Here is the relevant API route:
router.delete('/users/', (req, res) => {
const { id } = req.body;
User.findByIdAndDelete(id, (error, data) => {
if (error) {
console.log('error in deleting!');
throw error;
} else {
console.log('user has been deleted', data);
res.status(204).json(data);
}
});
});
It returns
DELETE /api/users/?id=5b34e5b5dfef8b4sd234567 204 47.816 ms - -
But when I GET users, the deleted user remains.
Here is the render I am mapping state into. I think I am pulling the id in properly but I am not sure what else to try:
{this.state.users.map(user => {
return (
<div className="Users1" key={user._id}>
<Card>
<CardBody>
<CardTitle>{user.username}</CardTitle>
<CardText>{user._id}</CardText>
<Button onClick={() => this.deleteUser(user._id)}
key={user._id}
type="button"
color="danger"
>
X
</Button>
</CardBody>
</Card>
</div>
);
})}
and here is state:
state = {
users: [],
id: ''
};
I had a similar problem. I had success by looking at the req.query property on the API server, rather than req.body:
const { id } = req.query;
I figured it out by dumping the entire req object to the console.
On the client side, my request did not include the id in the URL. I just included it in the params object. This seemed to help. So the equivalent call in your code would look something like this:
axios.delete(`${rootUrl}/api/users/`, {
params: { id }
})
I hope this helps. I'm still just learning React Native, so I'm hardly an expert. But I wanted to share what worked for me in case it helps others.