npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-ingredients

Dependencies

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 ProductIngredientsMobile

API 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 null if 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 fetched
  • error: Error object if the request fails
  • Null safety: Returns null for 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 productData changes
  • 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-htmlview for 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.