Apollo client useQuery makes additional call with the first page on every fetchMore call - apollo-client

There is a pagination example on the https://www.apollographql.com/docs/react/pagination/offset-based/.
Given there is a peace of code:
const { loading, data, fetchMore } = useQuery(FEED_QUERY, {
variables: {
offset: 0,
limit: 10,
},
});
...
const loadNextPage = useCallback(() => {
return fetchMore({
variables: {
offset: data.feed.length,
},
});
}, []);
...
<button onClick={loadNextPage}>Load more</button>;
When the component renders first time, the client makes the first page call as expected and 1 network call to the GraphQL endpoing is performed.
When I click the "Load more" button, the client makes 2 network calls: 1 for the first page data; 2 for the next 10 feeds.
If I click the "Load more" next time, I'll get the same situation as before — 2 network calls are performed (1 for the first page - 10 feeds, and 2 for the next 10 feeds).
I cannot get what is wrong with my configuration and why it calls the first page every time. It is unnecesarry.
There is some picularity there. The fetchPolicy is set to 'network-only' in the defaultOptions configuration of the ApolloClient:
query: {
fetchPolicy: 'network-only'
}

Related

How to wait for children to be greater than X in Cypress

I'm writing a test for a page, that loads the content via an AJAX-call. Here is the events:
Step 1: Page loads
Step 2: AJAX-call fetches content
Step 3: AJAX-call retrieves status 200
Step 4: X seconds passes
Step 5: Content is displayed on the page
My test is flaky, since it varies a lot how many seconds that passes in step 4, before the content is displayed.
Here is my code:
it( 'Test content is there', () => {
cy.fixture( 'my-fixture-path/article.json', "utf8" ).as( 'article' );
cy.get( "#article" ).then( ($article) => {
cy.intercept({
method: 'GET',
url: '/wp-json/my-namespace/public/v1/article/' + $article.postId,
}).as('contentApiCall');
});
cy.get( "#article" ).then( ($article) => {
cy.wait( '#contentApiCall' ).then( ({response}) => {
// This expect-line always passes
expect( response.statusCode ).to.equal( 200 );
// This check here is flaky, depending on how quickly the content renders
// after the API-call is returned.
cy.get( '#main .post-content' ).children().its( 'length' ).should( 'be.gte', 4 );
// The "Greater than 4" is because I'm checking for "More than 4 <p>-tags"
});
});
});
Can (and should) I change this line here:
cy.get( '#main .post-content' ).children().its( 'length' ).should( 'be.gte', 4 );
From:
'Are the length of #main .post-content-children more than 4'
To:
'Wait until the length of #main .post-content-children is more than 4'
?
... And if so - how do I do that?
Should assertions will continue to retry until the expected conditions are met. Now by default timeout is 4 seconds in cypress, but you can give custom timeouts for this.
cy.get('#main .post-content', {timeout: 7000})
.children({timeout: 7000})
.its('length')
.should('be.gte', 4)
Some times using .its('length') in a chain will break the retry mechanism.
You can fix it by using the callback version of .should()
cy.get( '#main .post-content' ).children()
.should( $children => {
const count = $children.length // move this inside the callback
expect(count).to.be.gte(4)
})

Query on page not refetching once navigating back to page

In my nextjs page I have the following hook (generated by using graphql-codegen) that fetches a graphql query.
const { data, error, loading, fetchMore, refetch, variables } = useGetShortlistQuery({
notifyOnNetworkStatusChange: true, // updates loading value
defaultOptions: {
variables: {
offset: undefined,
filterBy: undefined,
sortBy: SortBy.RecentlyAdded,
sortDirection: SortDirection.Desc,
},
},
});
This is the useGetShortlistQuery hook that is generated by graphql-codegen
export function useGetShortlistQuery(
baseOptions?: Apollo.QueryHookOptions<GetShortlistQuery, GetShortlistQueryVariables>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetShortlistQuery, GetShortlistQueryVariables>(GetShortlistDocument, options);
}
my component is wrapped in a HOC to enable Apollo Client
export default withApollo({ ssr: true })(Index);
The withApollo HOC uses #apollo/client and the cache property of the apollo client is as follows.
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
getShortlist: {
keyArgs: [],
merge(existing: PaginatedProperties | undefined, incoming: PaginatedProperties): PaginatedProperties {
return {
...incoming,
properties: [...(existing?.properties || []), ...(incoming?.properties || [])],
};
},
},
},
},
},
}),
The problem I am having is that on this page I update the variables on the useGetShortlistQuery using refetch which, in turn, updates the data.
However, if I navigate to another page, then come back to this page using this component. It doesn't seem to retrigger the graphql query so returns the previous data.
If you are using getStaticProps (or getServerSideProps) with pre rendered pages, it is a known behavior. It is due to optimisation by Next.js not re-rendering components between page navigations, with pages like [id].js.
The trick is to have a key on components that you want to see refreshing. You have multiple ways to do so. Having a different key on components tells React that it should be re-rendering the components, and thus will trigger again the hooks.
Practical example:
export const getStaticProps: GetStaticProps = async ({ params }) => {
const data = getData() //something that fetches your data here
return {
props: {
// some other data you want to return...
// some unique value that will be on each page
key: data.key
},
}
}
const MyPage: NextPage<InferGetStaticPropsType<typeof getStaticProps>> = (props) => {
<div key={props.key} />
}

Cypress Should fails test keeps running

I am writing a test to confirm page sizing is working properly and after I got the success I purposely configured the test to fail. The should fails, but the test just keeps running instead of failing out.
describe("Customers' Page size changes correctly", function () {
it("Should change the page size properly.", () => {
// SETTING UP THE INTERCEPT DETECTION THAT SAYS THAT THE PAGE HAS BEEN LOADED
cy.intercept(
"/api/v1/customers/?action=customers&pageSize=10&pageNumber=1&searchText=&filterByCustomerName=false&orderBy=CreatedOn&orderDirection=desc&partyDateStart=&partyDateEnd=&customerStatus=Active"
).as("rSearchRequest10");
cy.getToken().then(() => {
cy.visit("customers");
});
// Standard page size of 10
cy.wait("#rSearchRequest10").then(() => {
// Defaults to 10, should get 10 results.
const listResults = cy
.get("[data-testid=customersList]")
.find("[data-testid=customerListItem]");
assert.isNotEmpty(listResults);
listResults.should("have.length", 11);
});
});
});
I get the message
expected [ <div#eu.MuiBox-root.jss166>, 9 more... ] to have a length of 11 but got 10
Then the timer just keeps running. I have no further tests and feel the test should have failed at this point.
What does cy.getToken() look like? –
Cypress.Commands.add("getToken", () => {
cy.intercept('dc.services.visualstudio.com/v2/track', {
fixture: 'External/track-service-response.json'
});
cy.request('GET', 'test/bridge'); });
The solution is below. My final code looks like this. The expect within the then properly throws the error and stops the execution of that test.
it("Should default to page size 10", () => {
cy.intercept(
"/api/v1/customers/?action=customers&pageSize=10&pageNumber=1&searchText=&filterByCustomerName=false&orderBy=CreatedOn&orderDirection=desc&partyDateStart=&partyDateEnd=&customerStatus=Active"
).as("rSearchRequest10");
cy.getToken().then(() => {
cy.visit("customers");
});
// Standard page size of 10
cy.wait("#rSearchRequest10").then(() => {
// Defaults to 10, should get 10 results.
cy.get("[data-testid=customerListItem]").then((listing) => {
expect(listing).to.have.lengthOf(10, "Should be exactly 10 results");
});
});
});
Unsure why it continues to run but suspect part of your problem has to to with async nature of cypress selectors.
Instead of setting const listResults to a var you should chain off of the cy.get or .find with a new .then()
in this case it seems like you could do without the assert.isNotEmpty and just go straight to .should()
cy.get("[data-testid=customersList]")
.find("[data-testid=customerListItem]")
.should("have.length", 11);
You can also try using "expect":
// Standard page size of 10
cy.wait("#rSearchRequest10").then(() => {
// Defaults to 10, should get 10 results.
var searchResponse = cy.get("[data-testid=customerListItem]").then(listing => {
expect(listing).to.have.lengthOf(11, "Your error message for failing");
});
});

React component not receiving intermediate state when chaining actions in redux-saga

I have two actions TEST and TEST_DONE which both increment an id property in my redux state. I am using redux-saga to dispatch the second action TEST_DONE automatically whenever I dispatch the first action TEST from my component.
I expect the order of execution to go like this:
component renders with initial value of testState.id = 0
component dispatches TEST action
component re-renders with testState.id = 1
saga dispatches the TEST_DONE action
component re-renders with testState.id = 2
Instead my component only re-renders when testState.id is updated to 2. I can't see the 1 value in the getSnapshotBeforeUpdate function. It shows 0 as the previous prop.
Why does the prop jump from 0 to 2 without receiving 1 in between?
saga.js:
export function* TestSagaFunc() {
yield put({
type: actions.TEST_DONE
});
};
export default function* rootSaga() {
yield all([
yield takeEvery(actions.TEST, TestSagaFunc),
]);
};
action.js:
const actions = {
TEST: 'TEST',
TEST_DONE: 'TEST_DONE',
callTest: (id) => ({
type: actions.TEST,
payload: {
id
}
}),
};
export default actions;
reducer.js:
const initState = {
testState: {
id: 0
}
};
export default function TestReducers ( state=initState, { type, ...action}) {
switch(type) {
default:
return state;
case actions.TEST: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
};
case actions.TEST_DONE: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
}
};
};
console output from component getSnapshotBeforeUpdate
Summarizing my comments from the question:
The redux state is indeed being updated as you've seen, but a component is not guaranteed to render every intermediate state change based on the way react batches state changes. To test this you can try importing delay from redux-saga/effects and adding yield delay(1000); before calling yield put in TestSagaFunc so the two state updates don't get batched together.
This is just a trick to illustrate the effects of batching and almost certainly not what you want to do. If you need the intermediate state to be rendered you could dispatch TEST_DONE from the component being rendered with a useEffect (or componentDidUpdate) to ensure that the component went through one render cycle with the intermediate state. But there is no way to force your component to render intermediate reducer states that are batched together.

ngtable server side pagination

Hello I try figure out how make server side pagination with angularjs an ngtable.
I have two web services:
localhost:8080/app/api/period Method GET return json list of entities. As parameters are passed page number, range of start period and range when it stop.
localhost:8080/app/api/period/count Method GET return count of periods. As parameters are passed range of start period and range when it stop.
this.tableParams = new ngTableParams({
page: 1,
count: 10
}, {
counts: [10],
total: 0,
getData: function($defer, params) {
$http.get('/app/api/period', {params: {
pageNumber:params.page() - 1,
rangeStart:rangeStart,
rangeStop:rangeStop}})
.success(function(data, status) {
params.total($http.get('/app/api/period/count', {params: {
rangeStart:rangeStart,
rangeStop:rangeStop}}));
$defer.resolve(data);
});
}
});
Table params.total isn't updated corectly so data in table are displayed but pagination buttons aren't visible.
Could anybody explain me how to use $http.get inside of success listener of other $http.get in this case to get correctly setted params.total.
You don't see pagination buttons because your get() probably returns 10 for count because of your "rangeStart", "rangeStop" limit on server side and if you return 10 results out of 10 total there is nothing to paginate.
You can return 10 results per request but params.total should always be count of all results.
Anyway you don't need 2 get() calls when you can return it in one like this :D
{
"results": [
{
"id": "1437",
"task_started_at": "2014-06-09 12:25:25",
"task_finished_at": "2014-06-09 12:25:25"
},
{
"id": "1436",
"task_started_at": "2014-06-09 12:26:31",
"task_finished_at": "2014-06-09 12:26:31"
}
],
"total": 1027
}
And you code could look like this:
params.total(data.total);
$defer.resolve(data.results);
And also you don't need total because you will get it from seerver so remove:
total: 0;
Finaly your code with 2 get() calls could look something like this:
this.tableParams = new ngTableParams({
page: 1,
count: 10
}, {
getData: function($defer, params) {
$http.get('/app/api/period', {params: {
pageNumber:params.page() - 1,
rangeStart:rangeStart,
rangeStop:rangeStop}})
.success(function(data, status) {
params.total($http.get('/app/api/period/count'));
$defer.resolve(data);
});
}
});
where $http.get('/app/api/period/count') returns total number of records like 1234
Good luck
Did you try to use #ngTasty server side pagination?
It's way easier.
http://zizzamia.com/ng-tasty/directive/table-server-side
look the downside url, hope this can help you:
var Api = $resource('/data');
this.tableParams = new NgTableParams({
page: 1, // show first page
count: 10 // count per page
}, {
filterDelay: 300,
getData: function(params) {
// ajax request to api
return Api.get(params.url()).$promise.then(function(data) {
alert("1111");
params.total(90);
return data.results;
});
}
});
}

Resources