Problem
useCountries() currently uses local Vue provide/inject state directly inside the composable:
const _sharedCountries = inject("swCountries", ref());
provide("swCountries", _sharedCountries);
This is inconsistent with the shared-composable pattern already used elsewhere in the repo, for example useCart, usePrice, and modal composables via createSharedComposable.
It also means concurrent initial consumers can still trigger duplicate requests before the shared countries ref is populated.
On pages like templates/vue-starter-template/app/pages/account/login.vue, multiple consumers can mount at roughly the same time:
SharedCountryStateInput.vue calls useCountries() for country/state options.
registrationFormRules.ts calls useCountries() for country-state validation.
- Other address-related components can do the same on checkout/account pages.
Because each caller sees countries as empty before the first request resolves, duplicate Store API requests can be issued for the same criteria.
Proposal
Refactor useCountries() to follow the existing createSharedComposable pattern rather than managing sharing directly with provide/inject.
The expected shape should be similar to patterns already present in the repo:
import { createSharedComposable } from "@vueuse/core";
function _useCountries(criteria?: Schemas["Criteria"]): UseCountriesReturn {
// existing state/fetch/computed logic
}
export const useCountries = createSharedComposable(_useCountries);
While doing this, make sure concurrent initial fetches are deduplicated by storing an in-flight promise in the shared composable state:
const countriesRequest = ref<
Promise<operations["readCountry post /country"]["response"]> | null
>(null);
async function fetchCountries() {
if (countriesRequest.value) {
return countriesRequest.value;
}
const queryCriteria = defu(searchCriteria.value, {
associations: {
states: {},
},
} as Schemas["Criteria"]);
countriesRequest.value = (
cacheableReads
? apiClient.invoke("readCountryGet get /country", {
query: { _criteria: encodeForQuery(queryCriteria) },
})
: apiClient.invoke("readCountry post /country", {
body: queryCriteria,
})
).then((result) => {
countries.value = result.data.elements;
return result.data;
});
try {
return await countriesRequest.value;
} finally {
countriesRequest.value = null;
}
}
Avoid adding new provide/inject keys as the primary sharing mechanism. The goal is to align this composable with the shared-composable convention already used in the package.
Suggested test coverage
Add a regression test to packages/composables/src/useCountries/useCountries.test.ts verifying concurrent calls only invoke the API once:
it("deduplicates concurrent country fetches", async () => {
const { vm, injections } = await useSetup(() => useCountries(), {
apiClient: {
invoke: vi.fn().mockResolvedValue({ data: CountryMock }),
},
});
const [firstResult, secondResult] = await Promise.all([
vm.fetchCountries(),
vm.fetchCountries(),
]);
expect(injections.apiClient.invoke).toHaveBeenCalledTimes(1);
expect(firstResult).toStrictEqual(CountryMock);
expect(secondResult).toStrictEqual(CountryMock);
expect(vm.getCountries).toStrictEqual(CountryMock.elements);
});
Also consider a test that two separate components calling useCountries() receive the same shared state, matching the behavior expected from createSharedComposable.
Acceptance criteria
useCountries() uses createSharedComposable instead of direct provide/inject sharing.
- Multiple concurrent
fetchCountries() calls share one Store API request.
- Existing
cacheableReads behavior remains unchanged: GET when enabled, POST otherwise.
- Existing public return shape of
useCountries() remains unchanged.
- Add focused regression test coverage.
Related
This is separate from the deterministic _criteria encoding issue: #2553
Problem
useCountries()currently uses local Vue provide/inject state directly inside the composable:This is inconsistent with the shared-composable pattern already used elsewhere in the repo, for example
useCart,usePrice, and modal composables viacreateSharedComposable.It also means concurrent initial consumers can still trigger duplicate requests before the shared countries ref is populated.
On pages like
templates/vue-starter-template/app/pages/account/login.vue, multiple consumers can mount at roughly the same time:SharedCountryStateInput.vuecallsuseCountries()for country/state options.registrationFormRules.tscallsuseCountries()for country-state validation.Because each caller sees countries as empty before the first request resolves, duplicate Store API requests can be issued for the same criteria.
Proposal
Refactor
useCountries()to follow the existingcreateSharedComposablepattern rather than managing sharing directly with provide/inject.The expected shape should be similar to patterns already present in the repo:
While doing this, make sure concurrent initial fetches are deduplicated by storing an in-flight promise in the shared composable state:
Avoid adding new provide/inject keys as the primary sharing mechanism. The goal is to align this composable with the shared-composable convention already used in the package.
Suggested test coverage
Add a regression test to
packages/composables/src/useCountries/useCountries.test.tsverifying concurrent calls only invoke the API once:Also consider a test that two separate components calling
useCountries()receive the same shared state, matching the behavior expected fromcreateSharedComposable.Acceptance criteria
useCountries()usescreateSharedComposableinstead of direct provide/inject sharing.fetchCountries()calls share one Store API request.cacheableReadsbehavior remains unchanged: GET when enabled, POST otherwise.useCountries()remains unchanged.Related
This is separate from the deterministic
_criteriaencoding issue: #2553