Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix stale data when clicking back button to stateless app #2565

Merged
merged 15 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions src/features/formData/FormData.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -543,32 +543,31 @@ describe('FormData', () => {
const { mutations, queries } = await render();

await user.type(screen.getByTestId('obj2.prop1'), 'a');
await user.tab();
expect(screen.getByTestId('obj2.prop1')).toHaveValue('a');
expect(screen.getByTestId('hasUnsavedChanges')).toHaveTextContent('true');

expect(queries.fetchFormData).toHaveBeenCalledTimes(1);

// Pretending to handle the save operation
await waitFor(() => expect(mutations.doPostStatelessFormData.mock).toHaveBeenCalledTimes(1));
mutations.doPostStatelessFormData.resolve({ obj2: { prop1: 'a' } });

await waitFor(() => expect(screen.getByTestId('hasUnsavedChanges')).toHaveTextContent('false'));

await user.click(screen.getByRole('button', { name: 'Navigate to a different page' }));
await screen.findByText('something different');

// We have to resolve the save operation, as otherwise 'hasUnsavedChanges' will be 'true' when we navigate back
// as otherwise it would still be working on saving the form data (and form data is marked as unsaved until the
// save operation is finished).
expect(mutations.doPostStatelessFormData.mock).toHaveBeenCalledTimes(1);
mutations.doPostStatelessFormData.resolve();

await user.click(screen.getByRole('button', { name: 'Navigate back' }));
await screen.findByTestId('obj2.prop1');

// No need to re-fetch anymore, as the query cache is updated with the saved form data. This used to expect 2
// calls to fetchFormData, but now it's only 1.
expect(queries.fetchFormData).toHaveBeenCalledTimes(1);
// We tried to cache the form data, however that broke back button functionality for some apps.
// See this issue: https://github.com/Altinn/app-frontend-react/issues/2564
// Also see src/features/formData/useFormDataQuery.tsx where we prevent caching for statless apps
expect(queries.fetchFormData).toHaveBeenCalledTimes(2);

// No need to save the form data again, as it was already saved and nothing has changed since then.
expect(screen.getByTestId('obj2.prop1')).toHaveValue('a');
// Our mock fetchFormData returns an empty object, so the form data should be reset. Realistically, the form data
// would be restored when fetching it from the server, as we asserted that it was saved before navigating away.
expect(screen.getByTestId('obj2.prop1')).toHaveValue('');
expect(screen.getByTestId('hasUnsavedChanges')).toHaveTextContent('false');
expect(mutations.doPostStatelessFormData.mock).toHaveBeenCalledTimes(1);
});
});
});
17 changes: 15 additions & 2 deletions src/features/formData/useFormDataQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ export function useFormDataQueryDef(url: string | undefined): QueryDefinition<un
const { fetchFormData } = useAppQueries();
const queryKey = useFormDataQueryKey(url);
const options = useFormDataQueryOptions();
const isStateless = useApplicationMetadata().isStatelessApp;

const queryFn = url ? () => fetchFormData(url, options) : skipToken;

if (isStateless) {
// We need to refetch for stateless apps as caching will break some apps.
// See this issue: https://github.com/Altinn/app-frontend-react/issues/2564
return {
queryKey,
queryFn,
gcTime: 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skal ikke hindre merge, men bare noen tanker 😄

Kunne det vært nyttig å sette staleTime i stedet for gcTime for at senere fetches skal hente med stale-while-revalidate?

En annen tanke er også at isStateless-variabelen har noe å si hvor hva som returneres, og burde da kanskje være i queryKey-en? Typ [...queryKey, { isStateless }] eller noe sånn at man har forskjellig queryKey for stateful og stateless.

Copy link
Contributor Author

@adamhaeger adamhaeger Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hvis vi har har stale-while-revalidate, vil vi ikke da vise gammel data til vi har fått ny data? Det vil vi ikke i det tilfellet denne buggen kommer fra ihvertfall siden de bruker denne dataen til å vise om man har startet på skjemaet eller ikke. Vi ville da vist siden med "start nytt skjema" først, og så "Fortsett på skjema" når dataen er lastet.

Litt usikker på hva vi vinner på å ha isStateless i queryKey? At vi trigger query når isStateless forandrer seg?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query keyen bør allerede være forskjellig når man er i stateless siden query keyen er basert på url-en den henter ifra. For stateful er det url til data-elementet, men for stateless er det en annen url som refererer til data-typen som brukes i stateless (siden det ikke finnes noe data element da)

};
}

return {
queryKey,
queryFn: url ? () => fetchFormData(url, options) : skipToken,
enabled: !!url,
queryFn,
refetchInterval: false,
};
}
Expand Down
8 changes: 1 addition & 7 deletions test/e2e/integration/stateless-app/stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,8 @@ describe('Stateless', () => {
});

it('back button should work after starting an instance', () => {
cy.get(appFrontend.stateless.name).clear();
cy.get(appFrontend.stateless.name).type('hello world');
cy.get(appFrontend.stateless.number).clear();
cy.get(appFrontend.stateless.number).type('6789');
cy.get(appFrontend.instantiationButton).click();
cy.findByRole('textbox', { name: /id/i }).should('have.value', '1364'); // Make sure we are on the correct page
cy.window().then((win) => win.history.back());
cy.get(appFrontend.stateless.name).should('have.value', 'hello world');
cy.get(appFrontend.stateless.number).should('have.value', '6789');
cy.get(appFrontend.instantiationButton).should('exist');
});
});
Loading