Migrate to Storefront v8
This guide provides a comprehensive step-by-step process for migrating your Storefront Boilerplate-based application utilizing the SCAYLE Storefront SDK packages (storefront-core
& storefront-nuxt
) from version 7 to version 8. This major release marks a significant milestone in the evolution of SCAYLE Storefront, as we've undertaken a substantial architectural overhaul throughout the year. With the arrival of version 8, we're taking the opportunity to streamline the SDK packages, removing legacy code and deprecations, while also sunsetting support for outdated approaches like faceted search (e.g. useFacet
). This migration guide will act as your roadmap, ensuring a smooth transition to the enhanced performance, stability, and developer experience that SCAYLE Storefront v8 offers.
The complexity of your migration from SCAYLE Storefront v7 to v8 will depend significantly on the initial version of the SCAYLE Storefront Boilerplate used as the foundation for your project and update frequency. While projects consistently updated with the latest Storefront SDK versions will experience a smoother transition, those based on older versions, particularly Release Candidate (RC) versions of the SCAYLE Storefront Boilerplate, will require additional effort. This may involve manually incorporating architectural changes introduced in subsequent Boilerplate releases. Please assess your project's starting point to accurately estimate the migration scope and resources required.
A Short Overview of Breaking Changes
- 💥 Unified Storage Configuration: Legacy Storage Configuration Removed
- 💥 Migrate from
getBadgeLabel
: Implement Custom Badge Logic - 💥 Migrate to
shops
Configuration: Replacingstore
- 💥 Simplified Key Handling in RPC Composables:
key
value migrated to dedicated parameter - 💥 Updating API Configuration References: Transitioning from
bapi
tosapi
- 💥 Streamlining Cache Control: Replacing
AY_CACHE_DISABLED
withstorefront.cache.enabled
- 💥 Migrating to Enhanced Session Management: Now Opt-In
- 💥 Accessing Basket Data: New Response Structure and Error Handling
- 💥 Streamlining Data Fetching parameter: Transitioning to
immediate
and removingautoFetch
- 💥 Accessing User Access Tokens: Migrating to the getAccessToken RPC
- 💥 Standardized Error Handling in RPC Methods:
BAPIError
&BaseError
Removed - 💥 Simplified API Access in Multi-Shop Setups: Global API Routing for Path-Based Shops
- 💥 Search v1 replaced by Search v2: Removing
searchProducts
RPC in favor ofgetSearchSuggestions
- 💥Aligning with Nuxt
useAsyncData
: Replacing return valuespending
withstatus
andfetch
withrefresh
- 💥 Renaming of
useNavigationTree
: Now useNavigationTreeById - 💥 Removed Composable
useFacet
: Using dedicated Category-Specific Composables - 💥 Removed Composable
useQueryFilterState
: Improving Filter Management withuseFilter
anduseAppliedFilters
- 💥 Simplified IDP Integration: Using
loginIDP
for Callback Handling - 💥 RpcContext Cleanup: Removing Legacy Attributes
- 💥 Enhanced Key Security: Using SHA256 for Basket and Wishlist Key Generation
- 💥 Simplified Composable Caching:
enableDefaultGetCachedDataOverride
ReplacesdisableDefaultGetCachedDataOverride
- 💥 Migrating User Authentication: Removing deprecated
loginShopId
Attribute
Unified Storage Configuration: Legacy Storage Configuration Removed
We've streamlined the storage configuration process in v8. The deprecated transformLegacyConfig
function has been removed, replaced by the new storefront.storage
configuration system.
What this means for you:
- Legacy configuration options
provider
andredis
configuration are no longer supported and had been deprecated since Storefront Boilerplate v1.0.0-rc.05. - You'll need to migrate your existing storage settings to the new
storefront.storage
format.- Check Storefront Guide > Developer Guide > Basic Setup > Introduction - Storage for additional information.
These changes might impact your environment variables used for deployments. Please check your infrastructure and deployment setup and adapt accordingly!
Here's a quick example to guide you:
No Longer Supported: Legacy Storage Setup (nuxt.config.ts
):
export default {
// ...
runtimeConfig: {
// ...
storefront: {
// ...
redis: {
host: 'localhost',
port: 6379,
prefix: '',
user: '',
password: '',
sslTransit: false,
},
// ...
session: {
provider: 'redis',
},
// ...
},
// ...
},
// ...
}
Old Environment Variables (used during runtime) / .env
:
NUXT_STOREFRONT_REDIS_HOST=''
NUXT_STOREFRONT_REDIS_PORT=''
NUXT_STOREFRONT_REDIS_PREFIX=''
NUXT_STOREFRONT_REDIS_USER=''
NUXT_STOREFRONT_REDIS_PASSWORD=''
NUXT_STOREFRONT_REDIS_SSL_TRANSIT=true|false
NUXT_STOREFRONT_SESSION_PROVIDER='redis'
Migrate from getBadgeLabel
: Implement Custom Badge Logic
Starting with v8, we're empowering you to have more control over the display of badge labels within your applications.
What's changing?
- The
getBadgeLabel
helper function is no longer exported. - You will now implement the logic for generating your badge labels directly within your application code.
A detailed example of creating a "sustainability" badge can be found in our documentation: Getting Started > Quick Start > Create a Sustainability Badge.
Good News for Some: If your project uses the SCAYLE Storefront Boilerplate v1.0 or later, this change won't affect you, as the getBadgeLabel
function was already removed in that release.
For users with projects based on older boilerplate versions, or those directly utilizing getBadgeLabel
, you can refer to the previous implementation below as a reference for your custom implementation:
Reference Implementation: getBadgeLabel
const BadgeLabel = {
NEW: 'new',
SOLD_OUT: 'sold_out',
ONLINE_EXCLUSIVE: 'online_exclusive',
SUSTAINABLE: 'sustainable',
PREMIUM: 'premium',
DEFAULT: '',
} as const
type BadgeLabelParamsKeys =
| 'isNew'
| 'isSoldOut'
| 'isOnlineOnly'
| 'isSustainable'
| 'isPremium'
type BadgeLabelParams = Partial<Record<BadgeLabelParamsKeys, boolean>>
const getBadgeLabel = (params: BadgeLabelParams = {}): string => {
if (!params) {
return BadgeLabel.DEFAULT
}
const { isNew, isSoldOut, isOnlineOnly, isSustainable, isPremium } = params
if (isNew) {
return BadgeLabel.NEW
} else if (isSoldOut) {
return BadgeLabel.SOLD_OUT
} else if (isOnlineOnly) {
return BadgeLabel.ONLINE_EXCLUSIVE
} else if (isSustainable) {
return BadgeLabel.SUSTAINABLE
} else if (isPremium) {
return BadgeLabel.PREMIUM
} else {
return BadgeLabel.DEFAULT
}
}
💥 Migrate to shops
Configuration (Replacing store
)
We're streamlining how you configure multiple countries in v8. While we've been letting the old store
configuration option hang around for compatibility, it's time to fully embrace its future proof successor: shops
!
What's changing?
- The
store
option in your module configuration is officially retired. - You'll now use the
shops
keyword to configure your storefront settings.
This transition to the shops
configuration offers a more robust and scalable solution for handling multiple storefronts. You may already be familiar with this change as it was initially introduced in v7.84.0
(released 1. August 2024) on to ensure a smooth migration path.
These changes might impact your environment variables used for deployments. Please check your infrastructure and deployment setup and adapt accordingly!
Need a helping hand with the new configuration? Our documentation has you covered: Storefront Guide / Developer Guide / Basic Setup / Introduction - Shops
Here's a quick example to guide you:
nuxt.config.ts
:
export default {
// ...
runtimeConfig: {
// ...
storefront: {
// ...
shops: {
// ...
},
// ...
},
// ...
},
// ...
}
New Environment Variable structure (used during runtime) / .env
:
NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_USER=''
NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_TOKEN=''
NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_SECRET=''
NUXT_STOREFRONT_STORE_{UNIQUE_IDENTIFIER}_CHECKOUT_HOST=''
💥 Simplified Key Handling in RPC Composables: key
value migrated to a dedicated parameter
To better align with the current Nuxt 3 architecture and make key handling more explicit, we've simplified how you interact with the key
parameter in RPC composables from version 8 onwards.
What's changing?
- Key as a Direct Argument: Instead of placing the
key
within the composable'soptions
object, you'll now provide it as the second argument when calling the composable.
This adjustment promotes consistency and improves code readability by making the key's purpose more explicit.
Here's a quick example to guide you:
useProduct({
// ...
key: 'productKey',
})
💥 Updating API Configuration References: Transitioning from bapi
to sapi
We've updated our configuration to use sapi
(shorthand for Storefront API) consistently, replacing the deprecated bapi
keyword for improved clarity and consistency.
What's changing?
- All instances of
bapi
in the codebase have been replaced withsapi
. - The
initBapi
function has been removed and is no longer available. - The
bapiClient
property been replaced withsapiClient
within theRPCContext
.
These changes impact your environment variables used for deployments. Please check your infrastructure and deployment setup and adapt accordingly.
To make sure your projects are compatible with the latest version, we recommend updating your codebases as shown in the example:
nuxt.config.ts
:
export default {
// ...
runtimeConfig: {
// ...
storefront: {
// ...
bapi: {
host: '...',
token: '...',
},
// ...
},
// ...
},
// ...
}
Legacy Environment Variables (used during runtime) / .env
:
NUXT_STOREFRONT_BAPI_HOST='...'
NUXT_STOREFRONT_BAPI_TOKEN='...'
💥 Streamlining Cache Control: Replacing AY_CACHE_DISABLED
with storefront.cache.enabled
We've simplified the way you manage caching behavior in v8 by replacing the legacy AY_CACHE_DISABLED
environment variable with more intuitive options.
What's changing?
- The
AY_CACHE_DISABLED
environment variable is no longer supported. - You can now control cache behavior using either of the following:
- The
NUXT_STOREFRONT_CACHE_ENABLED
environment variable. - The
storefront.cache.enabled
option within yournuxt.config.ts
file.
- The
These changes provide greater flexibility and clarity when configuring your cache settings.
Environment Variables (used during runtime) / .env
:
AY_CACHE_DISABLED=true|false
💥 Migrating to Enhanced Session Management: Now Opt-In
We've improved session management for multi-storefront setups in @scayle/storefront-nuxt
to provide greater stability and ease of implementation.
What's changing?
- Starting with version
@scayle/[email protected]
(released on 13 May 2024), each shop utilizes a unique session cookie name instead of relying on thePath
attribute. This simplifies handling multiple storefronts and enhances session security. - The automatic migration of legacy session data introduced in
@scayle/[email protected]
(released on 13 May 2024) has been disabled by default and now need to have a feature flagstorefront.legacy.enableSessionMigration
enabled.
Storefront Config: storefront.legacy.enableSessionMigration
In nuxt.config.ts
:
export default {
runtimeConfig: {
storefront: {
// ...
legacy: {
enableSessionMigration: true,
},
// ...
},
},
}
Important considerations for upgrading and ensuring a smooth transition:
Direct upgrades from versions prior to @scayle/[email protected]
(released on 13 May 2024) will result in the loss of user sessions without enabling the storefront.legacy.enableSessionMigration
feature flag.
- Deploy version
@scayle/[email protected]
(released on 13 May 2024) or higher in your production environment or alternatively deploy version@scalye/[email protected]
or higher while enablingstorefront.legacy.enableSessionMigration
. - Keep this version running for a period exceeding your configured session TTL (time-to-live). This allows all legacy session data to migrate.
- Once the session TTL period has passed, you can safely disable
storefront.legacy.enableSessionMigration
Cookie Format Changes: This change primarily affects internal session management. However, for reference, here's a comparison of the old and new cookie formats:
Before @scayle/[email protected]
(released on 13 May 2024):
Set-Cookie: $session=s:fa3746f9-88c8-4065-a6c9-0c7bee473dd8.pSoaN6Q7iFHHyWKE7s9gQAqdDzGb9fS8a478P7PHLxw; Path=/de
💥 Accessing Basket Data: New Response Structure and Error Handling
This update changes how you interact with the basket through our API. Instead of receiving the basket object directly from certain methods, it will now be nested within the response body. We've also standardized error handling for adding items and improved how our useBasket
composable handles partial successes.
What's changing?
- The following methods no longer return the basket object directly:
getBasket
removeItemFromBasket
addItemsToBasket
addItemToBasket
- The basket object will now be accessible as a property within the response body of these methods.
addItemToBasket
andaddItemsToBasket
will now return HTTP400
for errors.- Error details for
addItemToBasket
andaddItemsToBasket
can be found in theerror
property of the response. useBasket
now gracefully handles cases where adding items to the basket results in a smaller quantity being added than originally requested by checking againstAddToBasketFailureKind
.
For reference, here's a comparison of the old and new implementation on how to utilize the newly returned HTTP code and `useBasket` quantity improvements for improved error handling:
Excerpt from utils/basket.ts
import {
type BasketItem,
type BasketResponseData,
FetchError,
getFirstAttributeValue,
HttpStatusCode,
} from '@scayle/storefront-nuxt'
// ...
export const getBasketToastErrorMessageKey = (error: unknown) => {
if (error instanceof FetchError) {
if (error.response.status === HttpStatusCode.PRECONDITION_FAILED) {
return 'basket.notification.add_to_basket_variant_out_of_stock_error'
} else if (error.response.status === HttpStatusCode.CONTENT_TOO_LARGE) {
return 'basket.notification.add_to_basket_max_basket_items_error'
}
}
return 'basket.notification.add_to_basket_error'
}
// ...
💥 Streamlining Data Fetching parameter: Transitioning to immediate
and removing autoFetch
As of @scayle/[email protected]
(released 7 June 2024), we've standardized on the immediate
option to align with the parameters used in Nuxt 3's useAsyncData
function. This change simplifies the data fetching process and ensures greater consistency across our composables.
What's changing?
- The
autoFetch
option withinuseRpc
has been deprecated and replaced withimmediate
. - This update affects all data fetching composables offered by
@scayle/storefront-nuxt
.
Here's a quick example to guide you:
useRpc('rpcMethod', key, params, { autoFetch: true })
useUser({ autoFetch: true })
💥 Accessing User Access Tokens: Migrating to the getAccessToken RPC
We've updated how you can access a user's access token. Instead of directly accessing the storefrontAccessToken
field on the UserAuthentication
interface, you will now need to utilize the dedicated getAccessToken
RPC. This change provides a more secure and streamlined approach to handling user tokens within the application.
What's changing?
- The
storefrontAccessToken
field is removed from theUserAuthentication
interface. - You must now use the
getAccessToken
RPC to retrieve the user's access token.
Here's a quick example to guide you:
const { data, fetching, fetch, error, status } = useRpc(
'getUser',
// ...
)
data.value.user.authentication.storefrontAccessToken
💥 Standardized Error Handling in RPC Methods: BAPIError
& BaseError
Removed
This update streamlines error handling in RPC methods for better performance and alignment with web standards.
What's changing?
- We're removing the special handling for
BAPIError
andBaseError
in RPC methods. - These custom error classes are now treated like any other
Error
object. - Uncaught
BAPIError
andBaseError
exceptions in RPC methods now result in a standard 500 status code response. BAPIError
andBaseError
have been removed from@scayle/storefront-nuxt
.- To specify a custom status code for an RPC method response, you should now return a
Response
object directly.
How does this impact you?
For most users accustomed to our core RPC methods, this change won't require any action. However, if you've implemented custom RPC methods, we recommend reviewing and updating their error handling to utilize the standard Response
object for custom status codes. This approach leverages the native capabilities of the Response
object and provides consistency across your application.
To illustrate this change, consider the following example:
function myCustomRpc() {
// ...
throw new BaseError(404)
}
💥 Simplified API Access in Multi-Shop Setups: Global API Routing for Path-Based Shops
We've updated how API routes are managed for shops using path-based selection (path
or path_except_default
).
What's changing?
- API routes are now mounted globally: Instead of being nested under each shop's path, API routes will be available under a single, global path (default:
/api
). - Shop-level overrides removed: The option to customize the
apiBasePath
on a per-shop basis has been removed.
This change aims to simplify API route management and improve consistency across multi-shop environments.
If you are purging the cache via the API, make sure you use the correct endpoint and include the X-Shop-Id
header.
curl -X POST 'http://localhost:3000/en/api/purge/all'
💥Aligning with Nuxt useAsyncData: Replacing return values pending
with status
and fetch
with refresh
We've given the useRpc
composable an upgrade! It now offers a smoother and more powerful data fetching experience, aligning perfectly with the latest Nuxt 3 useAsyncData
functionality.
What's changing?
- Unified Refresh: Say goodbye to the separate
fetch
function! You can now refresh data using a single, intuitiverefresh
function on theuseRpc
composable.1 - Enhanced Status Insights: The
pending
boolean flag is replaced by a more detailedstatus
return value. This provides a clearer view of the fetching lifecycle with states like:idle
,pending
,success
, anderror
. - Modernized Interface: The overall interface now mirrors the streamlined approach of Nuxt 3's
useAsyncData
, making for a more consistent and familiar developer experience.
This update not only impacts the useRpc
composable directly, but also extends to other RPC composables relying on it, such as useProducts
and useCategories
.
Here's a quick example of how to move from refresh()
to fetch()
:
Example from Storefront Boilerplate middleware/authGuard.global.ts
:
// ...
if (!nuxt.ssrContext && !userComposable.user.value) {
await userComposable.fetch()
}
// ...
Here's another quick example of how to move from pending / fetching
to status
:
Example from Storefront Boilerplate pages/c/[...categories]/[...slug]-[id].vue
:
// template
// ...
<CategoryBreadcrumbs
v-if="currentCategory"
:products-fetching="productsFetching"
:category="currentCategory"
:products-count="totalProductsCount"
/>
// ...
<ProductList
:products="products"
:pagination="pagination"
:current-category="currentCategory"
:loading="productsFetching"
class="mt-8"
@click:product="trackProductClick"
@intersect:row="trackViewListing"
/>
// ...
// script setup
const {
products,
pagination,
fetching: productsFetching,
totalProductsCount,
paginationOffset,
} = useProductsByCategory(currentCategoryId, route, {
// ...
const {
data: currentCategory,
fetching: isCategoryFetching,
error: categoryError,
} = currentCategoryPromise
const validateCategoryExists = async () => {
await currentCategoryPromise
if (categoryError.value || (!isCategoryFetching && !currentCategory.value)) {
throw createError({ statusCode: HttpStatusCode.NOT_FOUND, fatal: true })
}
}
💥 Moving to Search v2: ReplacingsearchProducts
and useSearch
As part of our transition to SCAYLE Search v2, we're relying on a new, streamlined search experience that prioritizes category pages. To simplify integration and ensure consistency, we're consolidating search functionality around the useStorefrontSearch
composable from the storefront-nuxt
package.
What's changing?
- Removal of
searchProducts
RPC method: This method is no longer available. - Introduction of
getSearchSuggestions
: Use this method for retrieving search suggestions provided by theuseStorefrontSearch
composable from thestorefront-nuxt
package. - Two Primary Suggestion Types:
- Product Suggestions: Triggered by entering a product ID (productId or referenceKey) in the search bar.
- Category Suggestions: Appear when typing category-like terms. These suggestions visually indicate applicable filters and redirect to filtered category pages upon selection.
- Replacement of
useSearch
withuseStorefrontSearch
: We're transitioning to theuseStorefrontSearch
composable for improved search functionality. - Introduction of the
useSearchData
composable: This composable simplifies search interactions by acting as a central proxy foruseStorefrontSearch
. It manages the searchQuery and provides access to computed search data, debounced suggestions, and resolved routes, enhancing code reusability and maintainability. useStorefrontSearch
composable consistency: The composableuseStorefrontSearch
now is consistent withuseRpc
return values. Thefetching
boolean is replaced bystatus
with statesidle
,pending
,error
andsuccess
.
For detailed implementation guidelines and further information, refer to Storefront Guide / Developer Guide / Features / Search.
Here's a quick example of how to migrate from the searchProducts
to getSearchSuggestions
RPC method:
Using searchProducts
RPC directly:
const getSearchSuggestionsRpc = useRpcCall('searchProducts')
data.value = await searchProducts({
term: String(searchQuery.value),
...params,
})
Here's a quick example of how to migrate from useSearch
to useStorefrontSearch
:
Using useSearch
(Search v1):
const searchTerm = ref()
const { data, resolveSearch } = useStorefrontSearch(searchTerm, { categoryId: ..., with: { ... } }, 'search')
//data contains search suggestions
resolveSearch()
//data contains search results
Here's a quick example of how to adapt the new return value changes of useStorefrontSearch
from fetching
to status
:
const {
data,
resolveSearch,
getSearchSuggestions,
fetching,
...searchData
} = useStorefrontSearch(searchQuery, { key })
fetching.value // true or false
💥 Renaming of useNavigationTree:
Now useNavigationTreeById
We've updated the useNavigationTree
composable for better clarity and functionality.
What's changing?
- The
useNavigationTree
composable is now renamed touseNavigationTreeById
. Its functionality, parameters and return values stay identical.
This change aims to improve the naming convention, making the purpose of the composable clearer.
💥 Removed Composable useFacet
: Using dedicated Category-Specific Composables
The previous reliance on the useFacet
composable for category-related product listings often led to unintended complexity. While aiming for versatility, its broad approach to faceting sometimes created excessive abstraction, making it difficult to grasp the underlying logic and implement customized solutions for specific category needs. Additionally, the internal state management within useFacet
could sometimes lead to challenges with data consistency and multiple sources of truth, potentially introducing subtle bugs. To address these concerns, we moved to a more focused strategy using dedicated composables.
What's changing?
- Instead of using
useFacet
for category-related product listings, you'll now use the specialized composables:useProductsByCategory
to fetch products within a specific category.useFilters
oruseProductListFilters
to manage and apply filters to your product list.
How to migrate:
Transitioning to these purpose-built composables provides clearer separation of concerns and potentially improves performance. You can find detailed usage examples and API documentation under Storefront Guide / Developer Guide / Features / Product Listing Page.
Here's a rough example of how to use the dedicated composables:
const {products, category, filters, ...} = useFacet()
We strongly encourage all projects to adopt these new composables as soon as possible. However, if immediate migration proves difficult, you can find the source code for the deprecated useFacet
composable in the section below:
Please be aware that useFacet
is no longer officially supported. Continuing to use it in your project is considered custom code and falls outside the scope of standard support.
Legacy Implementation: useFacet
(Source Code)
import type {
CacheOptions,
FilterParams,
ProductCategoryWith,
ProductWith,
} from '@scayle/storefront-core'
import { useState } from '#app/composables/state'
import { computed } from 'vue'
import {
useCategories,
useFilters,
useProducts,
useProductsCount,
} from '#storefront/composables'
import { extendPromise } from '@scayle/storefront-nuxt'
type FacetParams =
& Partial<{
with: {
product?: ProductWith
}
includedFilters: string[]
includeSoldOut: boolean
includeSellableForFree: boolean
}>
& { initialPath?: string }
type Options = Partial<{
params: FacetParams
/** @deprecated use the second argument of the composable to define the key */
key: string
}>
export function useFacet({
key: optionsKey,
params = { initialPath: '/' },
}: Options = {}, _key?: string) {
const key = optionsKey ?? _key ?? 'useFacet'
// Configurable options
const currentPath = useState<string>(
`${key}-path`,
() => params.initialPath ?? '/',
)
const currentPage = useState<number | undefined>(
`${key}-currentPage`,
() => 1,
)
const currentPerPage = useState(`${key}-itemsPerPage`, () => 20)
const currentWhereCondition = useState<FilterParams['where']>(
`${key}-whereCondition`,
)
const currentSorting = useState<FilterParams['sort']>(`${key}-sorting`)
const currentPricePromotionKey = useState<string>(`${key}-pricePromotionKey`)
const currentCacheParams = useState<CacheOptions | undefined>(
`${key}-currentCacheParams`,
)
const currentOrFiltersOperator = useState<FilterParams['orFiltersOperator']>(
`${key}-orFilterOperator`,
)
const productCountWhere = useState<FilterParams['where']>(
`${key}-productCountWhereCondition`,
)
const categoriesPromise = useCategories({
params: () => ({
path: currentPath.value,
includeHidden: (params.with?.product?.categories as ProductCategoryWith)
?.includeHidden || undefined,
}),
options: { immediate: false },
key: `${key}-categories`,
})
const productsPromise = useProducts({
params: () => ({
page: currentPage.value,
perPage: currentPerPage.value,
with: params.with?.product,
category: currentPath.value,
includeSoldOut: params.includeSoldOut,
includeSellableForFree: params.includeSellableForFree,
where: currentWhereCondition.value,
sort: currentSorting.value,
pricePromotionKey: currentPricePromotionKey.value,
cache: currentCacheParams.value,
orFiltersOperator: currentOrFiltersOperator.value,
}),
options: { immediate: false },
key: `${key}-products`,
})
const productsCountPromise = useProductsCount({
params: () => ({
category: currentPath.value,
includedFilters: params.includedFilters,
includeSoldOut: params.includeSoldOut,
includeSellableForFree: params.includeSellableForFree,
where: productCountWhere.value,
orFiltersOperator: currentOrFiltersOperator.value,
}),
options: { immediate: false },
key: `${key}-productCount`,
})
const filtersPromise = useFilters({
params: () => ({
category: currentPath.value,
includedFilters: params.includedFilters,
includeSoldOut: params.includeSoldOut,
includeSellableForFree: params.includeSellableForFree,
where: currentWhereCondition.value,
orFiltersOperator: currentOrFiltersOperator.value,
}),
options: { immediate: false },
key: `${key}-filters`,
})
const {
data: categoryData,
fetch: refreshCategories,
fetching: categoriesFetching,
error: categoriesError,
status: categoriesStatus,
} = categoriesPromise
const {
data: productData,
fetch: _refreshProducts,
fetching: productsFetching,
error: productError,
status: productStatus,
} = productsPromise
const {
data: productCountData,
fetch: _refreshProductCount,
fetching: productCountFetching,
error: productCountError,
status: productCountStatus,
} = productsCountPromise
const {
data: filterData,
fetch: refreshFilters,
fetching: filtersFetching,
error: filterError,
status: filterStatus,
} = filtersPromise
const categories = computed(() => categoryData.value?.categories)
const selectedCategory = computed(() => categoryData.value?.activeNode)
const pagination = computed(() => productData.value?.pagination)
const products = computed(() => productData.value?.products)
// const filters = computed(() => filtersData.value?.filters)
const filters = computed(() => filterData.value?.filters)
const unfilteredCount = computed(() => filterData.value?.unfilteredCount)
type ExtendedFilterParams = FilterParams & {
path: string
pricePromotionKey?: string
cache?: CacheOptions
}
const refreshProductCount = async ({
where = undefined,
}: FilterParams = {}): Promise<void> => {
productCountWhere.value = where
if (productCountFetching.value) {
return
}
await _refreshProductCount()
}
const fetchProducts = async ({
path,
page = 1,
perPage = 20,
where = undefined,
sort = undefined,
pricePromotionKey = '',
cache,
orFiltersOperator = undefined,
}: ExtendedFilterParams) => {
currentPath.value = path
currentPage.value = page
currentPerPage.value = perPage
currentWhereCondition.value = where
currentSorting.value = sort
currentPricePromotionKey.value = pricePromotionKey
currentCacheParams.value = cache
currentOrFiltersOperator.value = orFiltersOperator
if (
categoriesFetching.value ||
productsFetching.value ||
filtersFetching.value
) {
return
}
await Promise.all([
refreshCategories(),
_refreshProducts(),
refreshFilters(),
])
return true
}
const filterProducts = async ({
where = undefined,
page,
sort = undefined,
orFiltersOperator = undefined,
}: FilterParams) => {
currentWhereCondition.value = where
currentPage.value = page
currentSorting.value = sort
currentOrFiltersOperator.value = orFiltersOperator
await _refreshProducts()
}
const fetchPage = async (pageToFetch: number) => {
currentPage.value = pageToFetch
await _refreshProducts()
}
return extendPromise(
Promise.all([
categoriesPromise,
productsPromise,
productsCountPromise,
filtersPromise,
]).then(() => ({})),
{
// filter data
filters,
filtersFetching,
unfilteredCount,
filterStatus,
filterError,
// filter preview
productCountData,
refreshProductCount,
productCountFetching,
productCountError,
productCountStatus,
// product data
products,
pagination,
productsFetching,
filterProducts,
productError,
productStatus,
// category data
categories,
selectedCategory,
categoriesFetching,
categoriesError,
categoriesStatus,
// other
fetchProducts,
fetchPage,
},
)
}
💥 Removed Composable useQueryFilterState
: Improving Filter Management with useFilter
and useAppliedFilters
The previous useQueryFilterState
composable, while functional, lacked the granularity needed for streamlined and transparent filter management within our storefront applications. To address this, we're introducing two new composables: useFilter
and useAppliedFilters
. This separation allows for a more organized and understandable approach to handling filters, making it easier to customize their behavior and integrate them seamlessly into your components.
What's changing?
- We're replacing the
useQueryFilterState
composable with two more specialized options:useFilter
(see Reference Implementation below) provides a centralized way to fetch available filters, apply selections, reset filters, and manage filter-related logic. The exact application process depends on the type of filter being used.useAppliedFilters
(from@scayle/storefront-product-listing
) offers a direct way to access and work with the currently applied filters in the product listing context. It formats these active filters into aProductSearchQuery
object, crucial for fetching filtered results and understanding user selections. Additionally, it provides computed properties like the total count of applied filters.
Understanding the New Filter Behavior
The order of filters displayed in the Storefront Boilerplate is determined by a combination of the SCAYLE Panel configuration and the order of filters returned by the Storefront API. For more details check Storefront Guide / Developer Guide / Features / Pages / Product Listing Page.
const {
activeFilters,
applyFilters,
resetFilterUrl,
productConditions,
} = useQueryFilterState()
applyFilters({ brand: 23, sale: true, maxPrice: 100, minPrice: 0 })
Reference Implementation: useFilter.ts
import { extendPromise } from '@scayle/storefront-nuxt'
import { type MaybeRefOrGetter, readonly, ref } from 'vue'
import type { LocationQuery } from 'vue-router'
import type { RangeTuple } from '@scayle/storefront-product-listing'
import { useTrackingEvents, useToast } from '~/composables'
import { useRoute, useRouter } from '#app/composables/router'
import { useI18n } from '#i18n'
import {
useProductListFilter,
getNewQueryFilters,
getClearedFilterQueryByKey,
createNewBoolAttributeQuery,
createNewAttributeQuery,
createNewPriceQuery,
useAppliedFilters,
} from '#storefront-product-listing'
export function useFilter(
currentCategoryId?: MaybeRefOrGetter<number | undefined>,
options: { immediate?: boolean; keyPrefix?: string } = {},
) {
const route = useRoute()
const router = useRouter()
const areFiltersCleared = ref(false)
const areFiltersUpdated = ref(false)
const { appliedFiltersCount, appliedFilter } = useAppliedFilters(route)
const sort = ref(route.query.sort)
const filterData = useProductListFilter(route, currentCategoryId, options)
const { clearedPriceQuery } = filterData
const { trackFilterApply, trackFilterFlyout } = useTrackingEvents()
const { t } = useI18n()
const { show } = useToast()
const applyAttributeFilter = async (slug: string, id: number) => {
const filters = createNewAttributeQuery(route, appliedFilter.value, {
slug,
id,
})
trackFilterApply(slug, id.toString())
await applyFilters(filters)
}
const applyBooleanFilter = async (slug: string, value: boolean) => {
const filters = createNewBoolAttributeQuery(route, appliedFilter.value, {
slug,
value,
})
trackFilterApply(slug, value.toString())
await applyFilters(filters)
}
const applyPriceFilter = async (prices: RangeTuple) => {
const filters = createNewPriceQuery(route, appliedFilter.value, prices)
trackFilterApply('prices', prices.join(','))
await applyFilters(filters)
}
const onSlideInOpen = () => trackFilterFlyout('open', 'true')
const onSlideInClose = () => {
trackFilterFlyout('close', 'true')
const isSortUpdated = sort.value !== route.query.sort
if (areFiltersUpdated.value && isSortUpdated) {
show(t('filter.updated_notification_all'), { type: 'SUCCESS' })
areFiltersUpdated.value = false
sort.value = route.query.sort
} else if (areFiltersUpdated.value) {
show(t('filter.updated_notification_filter'), { type: 'SUCCESS' })
areFiltersUpdated.value = false
} else if (isSortUpdated) {
show(t('filter.updated_notification_sort'), { type: 'SUCCESS' })
sort.value = route.query.sort
}
}
const resetPriceFilter = async () => {
await applyFilters(clearedPriceQuery.value)
}
const resetFilter = async (key: string) => {
await applyFilters(getClearedFilterQueryByKey(route, key))
}
const resetFilters = async () => {
await applyFilters({})
areFiltersCleared.value = true
areFiltersUpdated.value = false
setTimeout(() => {
areFiltersCleared.value = false
}, 3000)
}
const applyFilters = async (filter?: LocationQuery, scrollToTop = true) => {
if (!filter) {
return
}
// Should not apply reset all filter if appliedFilter is empty
if (!appliedFiltersCount.value && !Object.keys(filter).length) {
return
}
const { page, ...query } = getNewQueryFilters(route, filter)
await router.push({ query })
areFiltersUpdated.value = true
if (scrollToTop) {
window.scroll({ behavior: 'smooth', top: 0 })
}
}
return extendPromise(filterData, {
onSlideInOpen,
onSlideInClose,
applyPriceFilter,
applyBooleanFilter,
applyAttributeFilter,
trackFilterFlyout,
resetFilters,
resetPriceFilter,
resetFilter,
areFiltersCleared: readonly(areFiltersCleared),
})
}
Reference Implementation: useFilter.test.ts
import { describe, it, vi, expect, beforeEach } from 'vitest'
import type { CentAmount } from '@scayle/storefront-nuxt'
import { useFilter } from './useFilter'
const mocks = vi.hoisted(() => {
return {
route: { query: {} },
router: { push: vi.fn() },
useAppliedFilters: {
productConditions: { value: '' },
appliedFilter: { value: {} },
appliedFiltersCount: { value: 0 },
},
useTrackingEvents: {
trackFilterApply: vi.fn(),
trackFilterFlyout: vi.fn(),
},
useProductListFilter: { clearedPriceQuery: { value: {} } },
useToast: { show: vi.fn() },
useI18n: { t: vi.fn().mockImplementation((key) => key) },
}
})
vi.mock('#app/composables/router', () => ({
useRoute: vi.fn().mockReturnValue(mocks.route),
useRouter: vi.fn().mockReturnValue(mocks.router),
}))
vi.mock('#i18n', () => ({
useI18n: vi.fn().mockReturnValue(mocks.useI18n),
}))
vi.mock('~/composables', () => ({
useTrackingEvents: vi.fn().mockReturnValue(mocks.useTrackingEvents),
useToast: vi.fn().mockReturnValue(mocks.useToast),
}))
vi.mock('#storefront-product-listing', async () => {
const actual = await vi.importActual('#storefront-product-listing')
return {
...actual,
useProductListFilter: vi.fn().mockReturnValue(mocks.useProductListFilter),
useAppliedFilters: vi.fn().mockReturnValue(mocks.useAppliedFilters),
}
})
describe('useFilter', () => {
beforeEach(() => {
mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }
mocks.useAppliedFilters.appliedFiltersCount.value = 0
mocks.route.query = {}
vi.clearAllMocks()
})
describe('onSlideInClose', () => {
it('should show toast message on filter applied and modal closed', async () => {
mocks.useAppliedFilters.appliedFilter.value = {
attributes: [
{ key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
],
}
const { applyAttributeFilter, onSlideInClose } = useFilter()
await applyAttributeFilter('newAttribute', 1)
onSlideInClose()
expect(mocks.useToast.show).toBeCalledWith(
'filter.updated_notification_filter',
{ type: 'SUCCESS' },
)
})
it('should show toast message on sort applied and modal closed', async () => {
mocks.route.query = {
sort: 'price',
}
const { onSlideInClose } = useFilter()
mocks.route.query = {
sort: 'new',
}
onSlideInClose()
expect(mocks.useToast.show).toBeCalledWith(
'filter.updated_notification_sort',
{ type: 'SUCCESS' },
)
})
it('should show toast message on filter and sort applied and modal closed', async () => {
mocks.useAppliedFilters.appliedFilter.value = {
attributes: [
{ key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
],
}
mocks.route.query = {
sort: 'price',
}
const { applyAttributeFilter, onSlideInClose } = useFilter()
mocks.route.query = {
...mocks.route.query,
sort: 'new',
}
await applyAttributeFilter('newAttribute', 1)
onSlideInClose()
expect(mocks.useToast.show).toBeCalledWith(
'filter.updated_notification_all',
{ type: 'SUCCESS' },
)
})
it('should not show toast message on modal closed', async () => {
const { onSlideInClose } = useFilter()
onSlideInClose()
expect(mocks.useToast.show).not.toBeCalled()
})
})
describe('applyAttributeFilter', () => {
it('should add new filter and value to query', () => {
mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }
const { applyAttributeFilter } = useFilter()
applyAttributeFilter('newAttribute', 1)
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[newAttribute]': '1',
},
})
})
it('should add new filter and value to query and merge with existing query', () => {
mocks.useAppliedFilters.appliedFilter.value = { attributes: [] }
mocks.route.query = {
'filters[otherAttributes]': '1, 2, 3',
sort: 'asc',
term: 'term',
}
const { applyAttributeFilter } = useFilter()
applyAttributeFilter('newAttribute', 1)
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[newAttribute]': '1',
'filters[otherAttributes]': '1, 2, 3',
sort: 'asc',
term: 'term',
},
})
})
it('should add new value to query', () => {
mocks.useAppliedFilters.appliedFilter.value = {
attributes: [
{ key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
],
}
const { applyAttributeFilter } = useFilter()
applyAttributeFilter('newAttribute', 1)
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[newAttribute]': '2,3,4,1',
},
})
})
it('should remove existing value from query', () => {
mocks.useAppliedFilters.appliedFilter.value = {
attributes: [
{ key: 'newAttribute', values: [2, 3, 4], type: 'attributes' },
],
}
const { applyAttributeFilter } = useFilter()
applyAttributeFilter('newAttribute', 2)
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[newAttribute]': '3,4',
},
})
})
it('should remove existing value and filter from query', () => {
mocks.useAppliedFilters.appliedFilter.value = {
attributes: [{ key: 'newAttribute', values: [2], type: 'attributes' }],
}
mocks.useAppliedFilters.appliedFiltersCount.value = 1
const { applyAttributeFilter } = useFilter()
applyAttributeFilter('newAttribute', 2)
expect(mocks.router.push).toBeCalledWith({ query: {} })
})
it('should reset page param when attribute filter is applied', () => {
mocks.route.query = {
page: '1',
}
const { applyAttributeFilter } = useFilter()
applyAttributeFilter('newAttribute', 1)
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[newAttribute]': '1',
},
})
})
})
describe('applyBooleanFilter', () => {
it('should add boolean filter and value to query if true', () => {
const { applyBooleanFilter } = useFilter()
applyBooleanFilter('newBool', true)
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[newBool]': 'true',
},
})
})
it('should remove boolean filter and value from query if false', () => {
mocks.useAppliedFilters.appliedFiltersCount.value = 1
mocks.useAppliedFilters.appliedFilter.value = {
attributes: [{ key: 'newBool', value: true, type: 'boolean' }],
}
const { applyBooleanFilter } = useFilter()
applyBooleanFilter('newBool', false)
expect(mocks.router.push).toBeCalledWith({
query: {},
})
})
it('should reset page param when boolean filter is applied', () => {
mocks.route.query = {
page: '1',
}
const { applyBooleanFilter } = useFilter()
applyBooleanFilter('newBool', true)
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[newBool]': 'true',
},
})
})
})
describe('applyPriceFilter', () => {
it('should apply prices correctly', () => {
const { applyPriceFilter } = useFilter()
applyPriceFilter([99 as CentAmount, 2345 as CentAmount])
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[minPrice]': '99',
'filters[maxPrice]': '2345',
},
})
})
it('should reset page param when price filter is applied', () => {
mocks.route.query = {
page: '1',
}
const { applyPriceFilter } = useFilter()
applyPriceFilter([99 as CentAmount, 2345 as CentAmount])
expect(mocks.router.push).toBeCalledWith({
query: {
'filters[minPrice]': '99',
'filters[maxPrice]': '2345',
},
})
})
})
describe('resetFilter', () => {
it('should reset correctly', () => {
mocks.useAppliedFilters.appliedFilter.value = {
attributes: [
{ key: 'newAttribute', values: [1, 2, 3, 4], type: 'attributes' },
],
}
mocks.useAppliedFilters.appliedFiltersCount.value = 1
mocks.route.query = {
'filters[newAttribute]': [1, 2, 3, 4],
page: '1',
}
const { resetFilter } = useFilter()
resetFilter('newAttribute')
expect(mocks.router.push).toBeCalledWith({
query: {},
})
})
it('should not reset if slug is not applied', () => {
const { resetFilter } = useFilter()
resetFilter('newAttribute')
expect(mocks.router.push).not.toBeCalledWith({
query: {},
})
})
})
describe('resetPriceFilter', () => {
it('should reset correctly', () => {
mocks.useAppliedFilters.appliedFiltersCount.value = 1
mocks.route.query = {
'filters[minPrice]': '1',
'filters[maxPrice]': '12',
}
const { resetPriceFilter } = useFilter()
resetPriceFilter()
expect(mocks.router.push).toBeCalledWith({
query: {},
})
})
it('should not reset if price is not applied', () => {
const { resetPriceFilter } = useFilter()
resetPriceFilter()
expect(mocks.router.push).not.toBeCalledWith({
query: {},
})
})
})
describe('resetFilters', () => {
it('should reset all correctly', () => {
mocks.useAppliedFilters.appliedFiltersCount.value = 3
mocks.route.query = {
'filters[minPrice]': '1',
'filters[maxPrice]': '12',
'filters[attribute]': '[1,2,3,4,]',
'filters[sale]': true,
}
const { resetFilters } = useFilter()
resetFilters()
expect(mocks.router.push).toBeCalledWith({
query: {},
})
})
it('should not reset if no filter is not applied', () => {
const { resetFilters } = useFilter()
resetFilters()
expect(mocks.router.push).not.toBeCalledWith({
query: {},
})
})
})
})
We strongly encourage a swift transition to useFilter
and useAppliedFilters
for a more robust filter experience. However, if immediate migration presents difficulties, you can find the source code for the deprecated useQueryFilterState
in the following section:
Please be aware that useQueryFilterState
is no longer officially supported. Continuing its use in your project is considered custom code and falls outside the scope of standard support.
Legacy Implementation: useQueryFilterState
(Source Code)
import {
type FilterParams,
type TransformedFilter,
type Filter,
isFilterActive,
transformToWhereCondition,
serializeFilters,
deserializeFilters,
type SortingValueKey,
getSortByValue,
} from '@scayle/storefront-core'
import { computed } from 'vue'
import { useRoute, useRouter } from '#app/composables/router'
import type { LocationQuery, LocationQueryValue } from '#vue-router'
const DEFAULT_QUERY_KEYS = ['page', 'sort', 'term']
type Options = {
defaultSort?: SortingValueKey
}
function queryValueToString(
value: LocationQueryValue | LocationQueryValue[],
): string | undefined {
return Array.isArray(value)
? value.map(v => v?.toString()).filter(Boolean).join(',')
: value?.toString()
}
const isEqual = <T>(x: T, y: T): boolean => {
if (Object.is(x, y)) {
return true
}
// For non-objects or null, use strict equality
if (
typeof x !== 'object' || x === null || typeof y !== 'object' || y === null
) {
return x === y
}
// Compare objects
const keys = Object.keys(x) as (keyof T)[]
return keys.length === Object.keys(y).length &&
keys.every(key => Reflect.has(y, key) && isEqual(x[key], y[key]))
}
export const getCurrentPage = (query: LocationQuery): number | undefined => {
return typeof query.page === 'string' ? parseInt(query.page, 10) : undefined
}
/** @deprecated The Storefront Boilerplate contains a `useQueryFilterState` composable you can use instead. **/
export function useQueryFilterState(options: Options = {}) {
const router = useRouter()
const route = useRoute()
const activeFilters = computed(() => {
const allowedFilters = Object.fromEntries(
Object.entries(route.query)
.filter(
([key]) => !DEFAULT_QUERY_KEYS.includes(key),
)
.map(([key, value]) => [key, queryValueToString(value)])
.filter(([_key, value]) => !!value),
)
return deserializeFilters(allowedFilters)
})
const isActiveFilter = (filter?: TransformedFilter) => {
if (!filter) {
return Object.values(activeFilters.value).length === 0
}
return isFilterActive(activeFilters.value, filter)
}
const applyFilters = async (filter?: Filter, scrollToTop = true) => {
const newQuery = {
sort: route.query.sort,
term: route.query.term,
...(filter ? serializeFilters(filter) : {}),
}
if (isEqual(route.query, newQuery)) {
return
}
await router.push({
query: { ...newQuery },
})
if (scrollToTop) {
window.scroll({
behavior: 'smooth',
top: 0,
})
}
}
const resetFilterUrl = async () => {
await router.replace({ query: { term: route.query.term } })
}
const generateFilterParams = (): FilterParams => {
return {
where: { ...transformToWhereCondition(activeFilters.value) },
page: getCurrentPage(route.query),
sort: getSortByValue(route.query?.sort || '', options?.defaultSort),
}
}
return {
activeFilters,
applyFilters,
isActiveFilter,
resetFilterUrl,
productConditions: computed(() => generateFilterParams()),
}
}
💥 Simplified IDP Integration: Using loginIDP for Callback Handling
We've optimized the Identity Provider (IDP) login process by replacing the handleIDPLoginCallback
RPC method from the useIDP
composable with the loginIDP
function within the useAuthentication
composable.
What's changing?
- The
handleIDPLoginCallback
RPC method is removed from theuseIDP
composable. - Use the
loginIDP
function from theuseAuthentication
composable for handling IDP login callbacks.
Here's a quick guide on updating your implementation:
const { handleIDPLoginCallback } = await useIDP()
watch(
() => route.query,
async (query) => {
if (query.code && isString(query.code)) {
await handleIDPLoginCallback(query.code)
}
},
{ immediate: true },
)
💥 RpcContext Cleanup: Removing Legacy Attributes
We're cleaning up the RpcContext
a bit to simplify its structure.
What's changing?
- The
isCmsPreview
attribute has been removed fromRpcContext
. - The
storeCampaignKeyword
attribute has been removed fromRpcContext
.- The
campaignKey
is now automatically determined by fetching campaign data through the Storefront API client by retrieving a list of all campaigns from the API. It then narrows down the list by filtering for campaigns that are still active, ensuring any returned campaign is currently running. These active campaigns are then sorted by their start date, ensuring chronological order. Finally, it iterates through the sorted campaigns to find the first one that is currently active, returning its key as an identifier. If no active campaign is found or an error occurs, it returns nothing. - The
storeCampaignKeyword
is also no longer used to determine the active campaign key. The functionality to run only certain campaigns for specific countries is now supported through the SCAYLE Panel out of the box.
- The
💥 Enhanced Key Security: Using SHA256 for Basket and Wishlist Key Generation
We've enhanced security for basket and wishlist keys by switching the default hashing algorithm from MD5
to the more robust SHA256
.
What's changing?
- Basket and wishlist keys are now generated using
SHA256
by default. - You can customize the hashing algorithm back to
MD5
if needed through thestorefront.appKeys.hashAlgorithm
property within yournuxt.config.ts
file.
export default defineNuxtConfig({
// ...
runtimeConfig: {
// ...
storefront: {
// ...
appKeys: {
// ...
hashAlgorithm: HashAlgorithm.MD5
},
// ...
},
// ...
},
// ...
})
💥 Simplified Composable Caching: enableDefaultGetCachedDataOverride
Replaces disableDefaultGetCachedDataOverride
We've clarified the configuration option for controlling default caching behavior in composables and made a slight adjustment to its logic, especially regarding the handling of shared state with useRpc
.
What's changing?
- Shared State: By default,
useRpc
utilizes a shared cache. This means all instances called with the same key will share the same data. - Configuration Rename: The configuration option
disableDefaultGetCachedDataOverride
has been renamed toenableDefaultGetCachedDataOverride
. - Reversed Logic:
- When
enableDefaultGetCachedDataOverride
isfalse
(Default):- The default caching behavior of Nuxt is used. This differs from v7 which overrode this behavior by default.
- During SSR,
useAsyncData
prioritizes data fetching from Nuxt's internal data stores (nuxtApp.payload.data
for SSR based on the providedkey
. - If no SSR data is found, it falls back to cached data from previous requests stored in
nuxtApp._asyncData[key]?.data.value
.
- When
enableDefaultGetCachedDataOverride
istrue
:- The default caching mechanism during SSR is overridden. This is identical to the default behavior in v7.
- Instead of using the built-in hydration data, a Storefront-specific
getCachedData
function will be used to fetch cached data:
- When
(key, nuxtApp) => {
const hydrationData = nuxtApp.isHydrating
? nuxtApp.payload.data[key]
: nuxtApp.static.data[key]
return hydrationData ??
nuxtApp._asyncData[key]?.data.value as ResponseT
}
To maintain your currently used caching behavior:
Simply change the value of disableDefaultGetCachedDataOverride
to its opposite in your nuxt.config.ts
file (e.g., from false
to true
).
export default defineNuxtConfig({
// ...
runtimeConfig: {
// ...
public: {
// ...
storefront: {
// ...
disableDefaultGetCachedDataOverride: true,
},
// ...
},
// ...
},
// ...
})
💥 Migrating User Authentication: Removing deprecated loginShopId
Attribute
This update removes the reliance on the loginShopId
attribute within the ShopUser
interface for managing user shop associations. We're transitioning to a more streamlined approach using session cookies.
What's changing?
- The
loginShopId
attribute will be removed from theShopUser
interface. - User shop association will now be managed through session cookies.
Footnotes
- You can now refresh data using the
refresh
function on theuseRpc
composable. ↩