Section Data Fetching
Table of Contents
- Section Data Fetching
Introduction
Weaverse's section components can fetch their own data, which provides several key advantages:
- Modularity: Each component manages its own data dependencies
- Performance: Only fetch what's needed when it's needed
- Maintainability: Data fetching logic lives with the component that uses it
- Reusability: Components can be used in multiple contexts with different data
This guide covers everything you need to know about fetching data in Weaverse section components.
Core Concepts
The Component Loader Pattern
Weaverse section components use the loader pattern for server-side data fetching:
// app/sections/featured-collection/index.tsximport type { ComponentLoaderArgs } from '@weaverse/hydrogen'
// Define the expected input data typetype FeaturedCollectionData = { collection: { handle: string }}
// Component implementation...
// Define the loader function to fetch dataexport let loader = async (args: ComponentLoaderArgs<FeaturedCollectionData>) => { const { weaverse, data } = args const { storefront } = weaverse // Access component settings through the data parameter const { collection } = data // Fetch and return the data return await storefront.query(COLLECTION_QUERY, { variables: { handle: collection.handle } })}
The loader function:
- Receives arguments via
ComponentLoaderArgs
- Accesses the Weaverse client and component data
- Fetches necessary data from APIs
- Returns data that will be automatically passed to the component as
props.loaderData
Type Safety with TypeScript
Using TypeScript with your component loaders provides several benefits:
import type { ComponentLoaderArgs } from '@weaverse/hydrogen'import type { CollectionQuery } from 'storefrontapi.generated'
// Define input data shapetype FeaturedCollectionData = { collection: { handle: string } productsToShow: number}
// Define what the loader returns (will be available as props.loaderData)type LoaderReturnType = { collection: CollectionQuery['collection']}
export let loader = async ( args: ComponentLoaderArgs<FeaturedCollectionData>): Promise<LoaderReturnType | null> => { // Implementation...}
By specifying the input and output types, you get:
- Auto-completion in your IDE
- Type checking during development
- Better documentation for component usage
- Clearer contract between components and their data
Data Sources
Shopify Storefront API
The most common data source for Weaverse components is Shopify's Storefront API:
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<ProductData>) => { const { storefront } = weaverse const { product } = data if (!product?.handle) return null return await storefront.query(PRODUCT_QUERY, { variables: { handle: product.handle, language: storefront.i18n.language, country: storefront.i18n.country } })}
// GraphQL query defined elsewhereconst PRODUCT_QUERY = `#graphql query Product($handle: String!, $language: LanguageCode, $country: CountryCode) @inContext(language: $language, country: $country) { product(handle: $handle) { id title description # Other fields... } }` as const
External APIs
Weaverse components can also fetch data from any external API using the fetchWithCache
utility:
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<WeatherWidgetData>) => { const { fetchWithCache, env } = weaverse const { location = 'New York' } = data try { return await fetchWithCache(`https://api.weather.example/forecast`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'API-Key': env.WEATHER_API_KEY }, body: JSON.stringify({ location }), // Use built-in caching strategy: weaverse.storefront.CacheShort() }) } catch (error) { console.error('Weather API error:', error) return { error: true, forecast: [] } }}
The fetchWithCache
function:
- Works like the standard
fetch
API - Adds Hydrogen's caching capabilities
- Makes external API calls more efficient
- Provides a consistent interface for all data fetching
Component Data and Settings
The data
argument in ComponentLoaderArgs
contains all the component's settings configured by merchants in the Weaverse editor:
type MapComponentData = { latitude: number longitude: number zoom: number showTraffic: boolean mapStyle: 'standard' | 'satellite' | 'terrain'}
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<MapComponentData>) => { const { fetchWithCache, env } = weaverse const { latitude, longitude, zoom, showTraffic, mapStyle } = data // Use component settings to customize the API request return await fetchWithCache( `https://api.maps.example/staticmap?lat=${latitude}&lng=${longitude}&zoom=${zoom}&traffic=${showTraffic}&style=${mapStyle}&key=${env.MAPS_API_KEY}` )}
Implementation Patterns
Basic Data Fetching
The simplest pattern is direct data fetching based on component settings:
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<ProductData>) => { const { storefront } = weaverse const { productHandle } = data return await storefront.query(PRODUCT_QUERY, { variables: { handle: productHandle } })}
Conditional Fetching
Often you'll need to conditionally fetch data based on component settings:
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<TestimonialData>) => { const { storefront, fetchWithCache, env } = weaverse const { source, productHandle, collectionHandle } = data // Choose data source based on component settings switch (source) { case 'product-reviews': return await fetchWithCache(`https://api.reviews.example/product/${productHandle}`, { headers: { 'API-Key': env.REVIEWS_API_KEY } }) case 'collection-reviews': return await fetchWithCache(`https://api.reviews.example/collection/${collectionHandle}`, { headers: { 'API-Key': env.REVIEWS_API_KEY } }) case 'store-testimonials': return await storefront.query(TESTIMONIALS_QUERY) default: return { testimonials: [] } }}
Parallel Data Fetching
For optimal performance, fetch multiple data sources in parallel:
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<ProductDetailData>) => { const { storefront, fetchWithCache, env } = weaverse const { productHandle } = data // Fetch from multiple sources simultaneously const [productData, reviewsData, inventoryData] = await Promise.all([ // Product data from Shopify storefront.query(PRODUCT_QUERY, { variables: { handle: productHandle } }), // Reviews from third-party API fetchWithCache(`https://api.reviews.example/products/${productHandle}`, { headers: { 'Authorization': `Bearer ${env.REVIEWS_API_KEY}` } }), // Inventory data from ERP system fetchWithCache(`https://api.inventory.example/stock-levels`, { method: 'POST', headers: { 'API-Key': env.INVENTORY_API_KEY }, body: JSON.stringify({ sku: productHandle }) }) ]) // Combine the results return { product: productData.product, reviews: reviewsData.reviews || [], inventory: inventoryData.stockLevels || {} }}
Error Handling
Robust error handling ensures your components degrade gracefully when APIs fail:
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<NewsData>) => { const { fetchWithCache } = weaverse const { source, category, count = 3 } = data try { const response = await fetchWithCache( `https://api.news.example/${source}?category=${category}&count=${count}` ) // Validate the response if (!response || !Array.isArray(response.articles)) { console.warn('News API returned invalid data format') return { articles: [], error: 'invalid_format' } } return { articles: response.articles, error: null } } catch (error) { console.error('News API error:', error) // Return structured error data for the component to handle return { articles: [], error: error instanceof Error ? error.message : 'unknown_error' } }}
Dependent Queries
Sometimes you need to fetch data sequentially, where one request depends on the results of another:
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<RelatedProductsData>) => { const { storefront } = weaverse const { productHandle } = data // Step 1: Get the main product to find its type const { product } = await storefront.query(PRODUCT_BASIC_QUERY, { variables: { handle: productHandle } }) if (!product) return { relatedProducts: [] } // Step 2: Use the product type to find related products const { products } = await storefront.query(RELATED_PRODUCTS_QUERY, { variables: { productType: product.productType, excludeId: product.id, first: 4 } }) return { relatedProducts: products.nodes }}
Data Revalidation
Weaverse provides a powerful mechanism to automatically refresh component data when specific settings change. This ensures that the displayed content always reflects the current configuration.
Understanding shouldRevalidate
The shouldRevalidate
property, when added to an input in your component schema, tells Weaverse to reload the component's data from its loader function whenever that specific input changes.
// In your component schemainspector: [ { group: 'Settings', inputs: [ { type: 'select', name: 'sortOrder', label: 'Sort Products By', defaultValue: 'best-selling', shouldRevalidate: true, // This triggers data reload when changed configs: { options: [ { value: 'best-selling', label: 'Best Selling' }, { value: 'newest', label: 'Newest' }, { value: 'price-low-high', label: 'Price: Low to High' }, { value: 'price-high-low', label: 'Price: High to Low' } ] } } ] }]
When a merchant changes the sortOrder
in the editor, Weaverse will:
- Update the component's data with the new value
- Re-run the component's loader function
- Refresh the component with the updated data
This creates a seamless experience where changes in the editor immediately update the displayed content.
Auto-Revalidating Inputs
Some input types automatically trigger revalidation without needing the shouldRevalidate
property:
product
- When selecting a different productcollection
- When selecting a different collectionblog
- When selecting a different blogproduct-list
- When selecting different productscollection-list
- When selecting different collections
These inputs deal with Shopify resources that typically require fresh data when changed.
Custom Revalidation Rules
You can create powerful data-driven components by combining shouldRevalidate
with component loaders:
// app/sections/dynamic-product-grid/schema.tsexport const schema = { // ... other schema properties inspector: [ { group: 'Content', inputs: [ { type: 'collection', name: 'collection', label: 'Collection', // No need for shouldRevalidate as collection inputs auto-revalidate }, { type: 'select', name: 'sortBy', label: 'Sort By', defaultValue: 'BEST_SELLING', shouldRevalidate: true, // Will reload data when changed configs: { options: [ { value: 'BEST_SELLING', label: 'Best Selling' }, { value: 'CREATED_AT', label: 'Newest' }, { value: 'PRICE', label: 'Price: Low to High' }, { value: 'PRICE_DESC', label: 'Price: High to Low' } ] } }, { type: 'range', name: 'productsToShow', label: 'Number of Products', defaultValue: 4, shouldRevalidate: true, // Will reload data when changed configs: { min: 2, max: 12, step: 1 } }, { type: 'select', name: 'viewStyle', label: 'View Style', defaultValue: 'grid', shouldRevalidate: false, // UI change only, no data reload needed configs: { options: [ { value: 'grid', label: 'Grid' }, { value: 'slider', label: 'Slider' } ] } } ] } ]}
// app/sections/dynamic-product-grid/index.tsimport type { ComponentLoaderArgs } from '@weaverse/hydrogen'
type ProductGridData = { collection: { handle: string } sortBy: 'BEST_SELLING' | 'CREATED_AT' | 'PRICE' | 'PRICE_DESC' productsToShow: number viewStyle: string}
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<ProductGridData>) => { const { storefront } = weaverse const { collection, sortBy, productsToShow } = data if (!collection?.handle) return { products: [] } // This query will re-run whenever collection, sortBy, or productsToShow changes // because they have shouldRevalidate: true in the schema return await storefront.query(COLLECTION_PRODUCTS_QUERY, { variables: { handle: collection.handle, sortKey: sortBy, first: productsToShow, language: storefront.i18n.language, country: storefront.i18n.country } })}
// Component implementation...
In this example:
- Changing the collection, sort order, or number of products triggers a data reload
- Changing the view style doesn't require new data, so
shouldRevalidate
is set tofalse
This optimizes performance by only reloading data when necessary, while ensuring that content stays fresh and relevant as merchants configure their components.
Caching Strategies
Weaverse components inherit Hydrogen's powerful caching system, allowing you to optimize performance based on how frequently your data changes.
Available Caching Options
Strategy | Cache Control Header | Best Used For |
---|---|---|
CacheShort() | public, max-age=1, stale-while-revalidate=9 | Frequently changing data (price, inventory) |
CacheLong() | public, max-age=3600, stale-while-revalidate=82800 | Rarely changing data (product details, images) |
CacheNone() | no-store | Personalized or uncacheable data |
CacheCustom() | Custom defined | Special caching requirements |
Custom Caching
For fine-tuned control over caching behavior:
export let loader = async ({ weaverse }: ComponentLoaderArgs<StockTickerData>) => { const { fetchWithCache, storefront } = weaverse // Fast-changing financial data needs custom caching return await fetchWithCache('https://api.stocks.example/ticker', { strategy: storefront.CacheCustom({ // Cache for 30 seconds maxAge: 30, // Allow stale content for up to 2 minutes while revalidating staleWhileRevalidate: 120, // If revalidation fails, serve stale content for up to 5 minutes staleIfError: 300, // Cache control mode mode: 'public' }) })}
Caching Best Practices
-
Match cache duration to data volatility:
- Product descriptions:
CacheLong()
- Prices and inventory:
CacheShort()
- User-specific content:
CacheNone()
- Product descriptions:
-
Use stale-while-revalidate for a balance of freshness and performance
-
Consider cache hierarchies when combining multiple data sources
-
Be mindful of API rate limits when setting short cache durations
-
Use cache debugging headers during development to verify caching behavior
Real-World Examples
E-commerce Examples
Product Recommendations Component
// app/sections/product-recommendations/index.tsximport type { ComponentLoaderArgs } from '@weaverse/hydrogen'import { RECOMMENDATIONS_QUERY } from '~/graphql/queries'
type RecommendationsData = { product: { id: string; handle: string } recommendationType: 'related' | 'complementary' | 'bestsellers' maxProducts: number}
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<RecommendationsData>) => { const { storefront } = weaverse const { product, recommendationType, maxProducts = 4 } = data if (!product?.id) return { recommendations: [] } switch (recommendationType) { case 'related': case 'complementary': return await storefront.query(RECOMMENDATIONS_QUERY, { variables: { productId: product.id, intent: recommendationType.toUpperCase(), count: maxProducts } }) case 'bestsellers': return await storefront.query(BESTSELLERS_QUERY, { variables: { count: maxProducts } }) }}
Content Integration
Blog Feed with Categories
// app/sections/blog-feed/index.tsximport type { ComponentLoaderArgs } from '@weaverse/hydrogen'import { BLOG_ARTICLES_QUERY } from '~/graphql/queries'
type BlogFeedData = { blog: { handle: string } category?: string tagsToInclude: string[] tagsToExclude: string[] postsCount: number sortBy: 'newest' | 'oldest' | 'title'}
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<BlogFeedData>) => { const { storefront } = weaverse const { blog, category, tagsToInclude = [], tagsToExclude = [], postsCount = 3, sortBy = 'newest' } = data if (!blog?.handle) return { articles: [] } // Get all articles for the blog const response = await storefront.query(BLOG_ARTICLES_QUERY, { variables: { blogHandle: blog.handle, first: 250 // Maximum to retrieve } }) // Client-side filtering and sorting (could be moved to a server query with a custom app extension) let articles = response.blog.articles.nodes // Filter by category if specified if (category) { articles = articles.filter(article => article.tags.includes(category) ) } // Filter by tags if (tagsToInclude.length > 0) { articles = articles.filter(article => tagsToInclude.some(tag => article.tags.includes(tag)) ) } if (tagsToExclude.length > 0) { articles = articles.filter(article => !tagsToExclude.some(tag => article.tags.includes(tag)) ) } // Sort articles switch (sortBy) { case 'newest': articles.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()) break case 'oldest': articles.sort((a, b) => new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime()) break case 'title': articles.sort((a, b) => a.title.localeCompare(b.title)) break } // Limit to requested count articles = articles.slice(0, postsCount) return { articles }}
Third-Party Services
Currency Converter Widget
// app/sections/currency-converter/index.tsximport type { ComponentLoaderArgs } from '@weaverse/hydrogen'
type CurrencyConverterData = { baseCurrency: string targetCurrencies: string[] showChart: boolean}
type ExchangeRateResponse = { base: string rates: Record<string, number> timestamp: number}
export let loader = async ({ weaverse, data }: ComponentLoaderArgs<CurrencyConverterData>) => { const { fetchWithCache, env, storefront } = weaverse const { baseCurrency = 'USD', targetCurrencies = ['EUR', 'GBP', 'JPY', 'CAD'], showChart = false } = data try { // Fetch current exchange rates const exchangeRates = await fetchWithCache<ExchangeRateResponse>( `https://api.exchangerate.host/latest?base=${baseCurrency}&symbols=${targetCurrencies.join(',')}`, { // Exchange rates change throughout the day, but we don't need to fetch every request strategy: storefront.CacheCustom({ maxAge: 900, // 15 minutes staleWhileRevalidate: 3600 // 1 hour }) } ) // Fetch historical data for chart if needed let historicalData = null if (showChart) { const today = new Date() const lastMonth = new Date(today.setMonth(today.getMonth() - 1)) const formattedDate = lastMonth.toISOString().split('T')[0] historicalData = await fetchWithCache( `https://api.exchangerate.host/timeseries?start_date=${formattedDate}&end_date=${new Date().toISOString().split('T')[0]}&base=${baseCurrency}&symbols=${targetCurrencies.join(',')}`, { // Historical data can be cached longer strategy: storefront.CacheLong() } ) } return { base: baseCurrency, rates: exchangeRates.rates, timestamp: exchangeRates.timestamp, historicalData: historicalData?.rates || null } } catch (error) { console.error('Currency API error:', error) return { base: baseCurrency, rates: {}, error: 'Failed to fetch exchange rates' } }}
Performance Optimization
To ensure your section components load quickly:
-
Use parallel fetching with
Promise.all()
for independent data sources -
Implement appropriate caching strategies based on data freshness requirements
-
Filter data server-side whenever possible to reduce payload size
-
Consider data dependencies to avoid waterfall requests
-
Return only what you need to minimize response size
-
Handle errors gracefully with fallback content
-
Monitor API response times and optimize slow requests
Troubleshooting
Common issues and their solutions:
1. Missing or undefined data
// Problem: Data is sometimes undefinedexport let loader = async ({ data }: ComponentLoaderArgs<ProductData>) => { // ❌ This might cause an error if data.product is undefined return await storefront.query(QUERY, { variables: { handle: data.product.handle } })}
// Solution: Add proper validationexport let loader = async ({ data }: ComponentLoaderArgs<ProductData>) => { // ✅ Check for existence before accessing properties if (!data?.product?.handle) return null return await storefront.query(QUERY, { variables: { handle: data.product.handle } })}
2. Type errors in loader data
Use TypeScript to catch issues early:
// Define explicit types for your component datatype ProductCarouselData = { products: Array<{ handle: string }> autoplay: boolean autoplaySpeed: number}
// Use the type in your loaderexport let loader = async ({ data }: ComponentLoaderArgs<ProductCarouselData>) => { // TypeScript will now validate that data matches the expected structure}
3. API rate limiting issues
Implement proper caching and error handling:
export let loader = async ({ weaverse }: ComponentLoaderArgs<ApiData>) => { const { fetchWithCache, storefront } = weaverse try { return await fetchWithCache('https://rate-limited-api.example/data', { // Use longer cache times for rate-limited APIs strategy: storefront.CacheCustom({ maxAge: 600, // 10 minutes staleWhileRevalidate: 3600, // 1 hour staleIfError: 86400 // 1 day - use stale data on errors for a while }) }) } catch (error) { // Check for rate limit errors if (error instanceof Error && error.message.includes('rate limit')) { console.warn('API rate limit reached, using fallback data') return getFallbackData() } throw error }}
Related Documents
To further enhance your understanding of Weaverse's data fetching capabilities and component development, explore these related guides:
- Component Schema - Learn how to define component schemas, including the
shouldRevalidate
property - Input Settings - Comprehensive guide to all input types available in Weaverse
- Rendering a Weaverse Page - Understanding how Weaverse pages are rendered
- Project Structure - Learn how the project is organized