@hg-storefront/product-ingredients
v0.0.29
Published
A React hook library for fetching and structuring product ingredients, nutrition, and allergy data from Vendure e-commerce products. This library provides a clean, type-safe interface for accessing product nutritional information while preserving CMS form
Downloads
66
Readme
@hg-storefront/product-ingredients
A React hook library for fetching and structuring product ingredients, nutrition, and allergy data from Vendure e-commerce products. This library provides a clean, type-safe interface for accessing product nutritional information while preserving CMS formatting.
Features
- Type-safe data access: Full TypeScript support with comprehensive interfaces
- Structured nutrition data: Validated nutrition values (energy, protein, carbs, etc.)
- Allergy information: Organized allergy data with "contains" and "traces of" categories
- CMS formatting preservation: Maintains HTML formatting from the CMS for ingredients
- Cross-platform compatibility: Works in both React web and React Native applications
- Loading and error states: Built-in loading and error handling
- Memoized performance: Optimized with React useMemo for efficient re-renders
Installation
yarn add @hg-storefront/product-ingredientsDependencies
This library requires the following peer dependencies:
@haus-storefront-react/hooks(^0.0.7-7)@haus-storefront-react/shared-types(^0.0.7-7)react(^18.0.0)react-dom(^18.0.0) - for React web applications
Usage
Basic Usage
import { useProductIngredients } from '@hg-storefront/product-ingredients'
function ProductDetails({ productId }: { productId: string }) {
const {
ingredients,
nutrition,
allergies,
isLoading,
error
} = useProductIngredients({ productId })
if (isLoading) return <div>Loading product information...</div>
if (error) return <div>Error loading product data</div>
return (
<div>
{/* Your UI implementation */}
</div>
)
}React Web Example
import { useProductIngredients } from '@hg-storefront/product-ingredients'
import * as Accordion from '@radix-ui/react-accordion'
function ProductIngredientsAccordion({ productId }: { productId: string }) {
const { ingredients, nutrition, allergies, isLoading, error } = useProductIngredients({ productId })
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading product data</div>
return (
<Accordion.Root type="single" collapsible>
{/* Ingredients */}
{ingredients && (
<Accordion.Item value="ingredients">
<Accordion.Trigger>Ingredienser</Accordion.Trigger>
<Accordion.Content>
<div dangerouslySetInnerHTML={{ __html: ingredients.htmlContent }} />
</Accordion.Content>
</Accordion.Item>
)}
{/* Nutrition */}
{nutrition && (
<Accordion.Item value="nutrition">
<Accordion.Trigger>Näringsinnehåll</Accordion.Trigger>
<Accordion.Content>
<table>
<tbody>
<tr>
<td>Energi</td>
<td>{nutrition.energyKj} kj/{nutrition.energyKcal} kcal</td>
</tr>
<tr>
<td>Protein</td>
<td>{nutrition.protein} g</td>
</tr>
<tr>
<td>Kolhydrater</td>
<td>{nutrition.carbohydrates} g</td>
</tr>
<tr>
<td>Varav sockerarter</td>
<td>{nutrition.sugar} g</td>
</tr>
<tr>
<td>Fett</td>
<td>{nutrition.fat} g</td>
</tr>
<tr>
<td>Varav mättat fett</td>
<td>{nutrition.saturatedFat} g</td>
</tr>
<tr>
<td>Salt</td>
<td>{nutrition.salt} g</td>
</tr>
</tbody>
</table>
</Accordion.Content>
</Accordion.Item>
)}
{/* Allergies */}
{allergies.map((allergy, index) => (
<Accordion.Item key={index} value={`allergies-${index}`}>
<Accordion.Trigger>{allergy.heading}</Accordion.Trigger>
<Accordion.Content>
<p>{allergy.items.join(', ')}</p>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>
)
}React Native Example
import React from 'react'
import { View, Text, ScrollView, StyleSheet } from 'react-native'
import { useProductIngredients } from '@hg-storefront/product-ingredients'
import { Collapsible } from 'react-native-collapsible'
function ProductIngredientsMobile({ productId }: { productId: string }) {
const { ingredients, nutrition, allergies, isLoading, error } = useProductIngredients({ productId })
if (isLoading) return <Text>Loading...</Text>
if (error) return <Text>Error loading product data</Text>
return (
<ScrollView style={styles.container}>
{/* Ingredients */}
{ingredients && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Ingredienser</Text>
<Text style={styles.content}>
{/* Note: In React Native, you'll need to parse HTML or use a library like react-native-htmlview */}
{ingredients.htmlContent.replace(/<[^>]*>/g, '')}
</Text>
</View>
)}
{/* Nutrition */}
{nutrition && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Näringsinnehåll</Text>
<View style={styles.nutritionTable}>
<View style={styles.nutritionRow}>
<Text style={styles.nutritionLabel}>Energi</Text>
<Text style={styles.nutritionValue}>
{nutrition.energyKj} kj/{nutrition.energyKcal} kcal
</Text>
</View>
<View style={styles.nutritionRow}>
<Text style={styles.nutritionLabel}>Protein</Text>
<Text style={styles.nutritionValue}>{nutrition.protein} g</Text>
</View>
<View style={styles.nutritionRow}>
<Text style={styles.nutritionLabel}>Kolhydrater</Text>
<Text style={styles.nutritionValue}>{nutrition.carbohydrates} g</Text>
</View>
<View style={styles.nutritionRow}>
<Text style={styles.nutritionLabel}>Varav sockerarter</Text>
<Text style={styles.nutritionValue}>{nutrition.sugar} g</Text>
</View>
<View style={styles.nutritionRow}>
<Text style={styles.nutritionLabel}>Fett</Text>
<Text style={styles.nutritionValue}>{nutrition.fat} g</Text>
</View>
<View style={styles.nutritionRow}>
<Text style={styles.nutritionLabel}>Varav mättat fett</Text>
<Text style={styles.nutritionValue}>{nutrition.saturatedFat} g</Text>
</View>
<View style={styles.nutritionRow}>
<Text style={styles.nutritionLabel}>Salt</Text>
<Text style={styles.nutritionValue}>{nutrition.salt} g</Text>
</View>
</View>
</View>
)}
{/* Allergies */}
{allergies.map((allergy, index) => (
<View key={index} style={styles.section}>
<Text style={styles.sectionTitle}>{allergy.heading}</Text>
<Text style={styles.content}>{allergy.items.join(', ')}</Text>
</View>
))}
</ScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8,
},
content: {
fontSize: 16,
lineHeight: 24,
},
nutritionTable: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
},
nutritionRow: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
nutritionLabel: {
fontSize: 16,
flex: 1,
},
nutritionValue: {
fontSize: 16,
fontWeight: '500',
},
})
export default ProductIngredientsMobileAPI Reference
Hook: useProductIngredients
Parameters
interface UseProductIngredientsProps {
productId: string
}Returns
interface UseProductIngredientsReturn {
productData: Product | undefined
isLoading: boolean
error: unknown
ingredients: IngredientsData | null
nutrition: NutritionData | null
allergies: AllergyData[]
}Data Types
IngredientsData
interface IngredientsData {
htmlContent: string // Preserves CMS formatting
}NutritionData
interface NutritionData {
energyKj: number
energyKcal: number
protein: number
carbohydrates: number
sugar: number
fat: number
saturatedFat: number
salt: number
}AllergyData
interface AllergyData {
heading: string // "Innehåller" or "Spår av"
items: string[] // Array of allergy items
}Data Structure
Ingredients
htmlContent: String containing HTML from CMS that preserves formatting (bold, italic, line breaks, etc.)- Note for React Native: HTML content needs to be parsed or stripped of tags for display
Nutrition
- Structured nutrition values: All values are validated numbers representing grams (g) or kilojoules/kilocalories
- Complete nutrition profile: Energy (kJ/kcal), protein, carbohydrates, sugar, fat, saturated fat, and salt
- Validation: Returns
nullif any nutrition values are missing or invalid
Allergies
- Organized by type: Separated into "Innehåller" (Contains) and "Spår av" (Traces of) categories
- Facet-based: Data is extracted from Vendure facet values with codes
'contains'and'traces-of' - Array structure: Each allergy group has a heading and an array of allergy items
Error Handling
The hook provides comprehensive error handling:
isLoading: Boolean indicating if data is being fetchederror: Error object if the request fails- Null safety: Returns
nullfor missing or invalid data instead of throwing errors - Type validation: Validates nutrition data types before returning
Performance Considerations
- Memoized calculations: All data processing is memoized with
useMemo - Efficient re-renders: Only recalculates when
productDatachanges - Type safety: Prevents unnecessary re-renders through proper TypeScript typing
Browser/Platform Compatibility
- React Web: Full compatibility with all modern browsers
- React Native: Compatible with iOS and Android
- HTML rendering: For React Native, consider using libraries like
react-native-htmlviewfor ingredients display
Dependencies
This library depends on the Haus Storefront React ecosystem:
@haus-storefront-react/hooks: For product data fetching@haus-storefront-react/shared-types: For TypeScript type definitions
Make sure your project has these dependencies properly configured.
