How to differentiate prefetch requests from normal fetch requests? - redux-toolkit

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?

Related

Mochawesome with Cypress - how to get aggregated charts at higher level?

I've just started using mochawesome with Cypress (9.7). Our test structure is basically a number of spec files, each following something like the following format:
describe('(A): description of this spec', () => {
describe ('(B): description of test abc', () => {
before(() => {
// do specific set up bits for this test
})
it('(C): runs test abc', () => {
// do actual test stuff
})
})
})
Where within each spec file there would be a single 'A' describe block, but there can be many 'B' level blocks (each with a single 'C') - done this way because the before block for each 'C' is always different - I couldn't use a beforeEach.
When I run my various spec files, each structured similarly to the above, the mochaewsome output is mostly correct - I get a collapsible block for each spec file at level 'A', each with multiple collapsible blocks at level B, each with test info as expected at level C.
But... The circular charts are only displayed at level B. What I was hoping, was that it might be possible to have aggregated charts at level A, and a further aggregated chart for all the level A blocks.
Not sure I've explained this brilliantly(!), but hopefully someone understands, and can offer a suggestion?!
In cypress-mochawesome-reporter there's an alternative setup using on('after:run') which can perform the aggregation.
In Cypress v9.7.0
// cypress/plugins/index.js
const { beforeRunHook, afterRunHook } = require('cypress-mochawesome-reporter/lib');
const { aggregateResults } = require('./aggregate-mochawesome-report-chart');
module.exports = (on, config) => {
on('before:run', async (details) => {
await beforeRunHook(details);
});
on('after:run', async () => {
aggregateResults(config)
await afterRunHook();
});
};
In Cypress v10+
// cypress.config.js
const { defineConfig } = require('cypress');
const { beforeRunHook, afterRunHook } = require('cypress-mochawesome-reporter/lib');
const { aggregateResults } = require('./aggregate-mochawesome-report-chart');
module.exports = defineConfig({
reporter: 'cypress-mochawesome-reporter',
video: false,
retries: 1,
reporterOptions: {
reportDir: 'test-report',
charts: true,
reportPageTitle: 'custom-title',
embeddedScreenshots: true,
inlineAssets: false,
saveAllAttempts: false,
saveJson: true
},
e2e: {
setupNodeEvents(on, config) {
on('before:run', async (details) => {
await beforeRunHook(details);
});
on('after:run', async () => {
aggregateResults(config)
await afterRunHook();
});
},
},
});
The module to do the aggregation is
// aggregate-mochawesome-reporter-chart.js
const path = require('path');
const fs = require('fs-extra')
function aggregateResults(config) {
const jsonPath = path.join(config.reporterOptions.reportDir , '/.jsons', '\mochawesome.json');
const report = fs.readJsonSync(jsonPath)
const topSuite = report.results[0].suites[0]
aggregate(topSuite)
fs.writeJsonSync(jsonPath, report)
}
function aggregate(suite, level = 0) {
const childSuites = suite.suites.map(child => aggregate(child, ++level))
suite.passes = suite.passes.concat(childSuites.map(child => child.passes)).flat()
suite.failures = suite.failures.concat(childSuites.map(child => child.failures)).flat()
suite.pending = suite.pending.concat(childSuites.map(child => child.pending)).flat()
suite.skipped = suite.skipped.concat(childSuites.map(child => child.skipped)).flat()
if (!suite.tests.length && suite.suites[0].tests.length) {
// trigger chart when to describe has no tests
suite.tests = [
{
"title": "Aggregate of tests",
"duration": 20,
"pass": true,
"context": null,
"err": {},
"uuid": "0",
"parentUUID": suite.uuid,
},
]
}
return suite
}
module.exports = {
aggregateResults
}
The function aggregate() recursively loops down through child suites and adds the test results to the parent.
json files
Note the json file is different at the point where afterRunHook runs and at the end of the test run.
If you have the option saveJson: true set, you will get a final json file in the report directory called index.json.
At the afterRunHook stage the file is mochawesome.json.
Before aggregation
After aggregation

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

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

Two children with the same key mongodb _id

i got this error : Encountered two children with the same key, 610bebf8b6f3820b38b0c613. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version
but i use mongodb id so no way 2 id be them same
it happen when i load more items...
im using redux toolkit express mongoose
it happen sometimes not always
flatList :
<View style={{flex: 1, margin: 10, flexDirection: 'column'}}>
<Search />
{users.pending && !users.users.length ? (
<ActivityIndicator size="large" color="#0000ff" />
) : users.users.length === 0 ? (
<Text>No users</Text>
) : (
<FlatList
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
data={users.users}
renderItem={({item}) => <Card user={item} />}
keyExtractor={item => item._id}
onEndReachedThreshold={0}
onEndReached={handleLoadMore}
/>
)}
</View>
usersSlice :
getUsersSuccess: (state, action) => {
state.pending = false;
if (state.users.length === 0) {
state.users = action.payload;
} else {
state.users = [...state.users, ...action.payload];
}
state.skip = state.users.length;
},
apiCall
export const getUsers = async (dispatch, skip) => {
dispatch(getUsersStart());
try {
const res = await axios.get(`http://10.0.2.2:5000/users?skip=${skip}`);
dispatch(getUsersSuccess(res.data));
} catch (error) {
dispatch(getUsersError());
console.error('error => ', error.message);
}
};
server :
export const getUsers = async (req, res) => {
const skip =
req.query.skip && /^\d+$/.test(req.query.skip) ? Number(req.query.skip) : 0;
console.log(' req.qury.skip >>>>', req.query.skip);
try {
const users = await User.find({}, undefined, { skip, limit: 7 });
users.map((user) => {
user.name = decryptString(user.name, process.env.CRYPTO_PASSWORD);
});
res.status(200).json(users);
} catch (error) {
console.error(error.message);
res.status(500).json('Server Error');
}
};
i get
virtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc.
i think about delete duplicate item but i think not good choice
but i use mongodb id so no way 2 id be them same it happen when i load more items
your assumption can be wrong in multiple ways. Mongo IDs are unique in the database, but you could be getting duplicate values because of a variety of reasons.
I suspect that the API is giving you overlapping data and you merge new data with the old data without checking -
state.users = [...state.users, ...action.payload];
modify this to have a basic uniqueness check - and only add elements that are not already in the old list

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