The Liquid-React Bridge Beginner 10 min read

Data Attributes Pattern: Simple Component Hydration

Use HTML data attributes to pass simple values from Liquid to React components. Perfect for IDs, flags, and configuration options.

Data attributes are the simplest way to pass information from Liquid to React. They’re perfect for configuration options, IDs, and boolean flags that your components need at mount time.

How Data Attributes Work

HTML5 data attributes are custom attributes prefixed with data-. JavaScript can read them via the dataset property:

<div id="my-component" data-user-id="12345" data-is-admin="true" data-theme="dark"></div>
const element = document.getElementById('my-component');
// Dataset converts kebab-case to camelCase automatically.
console.log(element.dataset.userId); // "12345" (string!)
console.log(element.dataset.isAdmin); // "true" (string, NOT boolean!)
console.log(element.dataset.theme); // "dark"
// Remember: All dataset values are strings - parse manually if needed.

Key detail: data-user-id becomes dataset.userId (kebab-case to camelCase).

Basic Liquid to React Example

Liquid Template:

{%- comment -%} shopify-theme/sections/quantity-selector.liquid {%- endcomment -%}
<div
id="quantity-selector-{{ section.id }}"
class="quantity-selector"
data-line-id="{{ line_item.key }}"
data-current-quantity="{{ line_item.quantity }}"
data-max-quantity="{{ line_item.variant.inventory_quantity | default: 99 }}"
data-inventory-policy="{{ line_item.variant.inventory_policy }}"
></div>

React Component:

src/components/cart/QuantitySelector.tsx
import { useState, useEffect } from 'react';
interface QuantitySelectorProps {
rootId: string; // ID of the element containing data attributes.
}
export function QuantitySelector({ rootId }: QuantitySelectorProps) {
const [quantity, setQuantity] = useState(1);
const [maxQuantity, setMaxQuantity] = useState(99);
const [lineId, setLineId] = useState('');
// Read data attributes on mount.
useEffect(() => {
const root = document.getElementById(rootId);
if (!root) return;
// Destructure data attributes from the element's dataset.
// All values are strings - parse as needed.
const { lineId, currentQuantity, maxQuantity, inventoryPolicy } = root.dataset;
setLineId(lineId || '');
setQuantity(parseInt(currentQuantity || '1', 10)); // Parse string to number.
// Only limit quantity if inventory is tracked.
// 'continue' policy allows overselling.
if (inventoryPolicy !== 'continue') {
setMaxQuantity(parseInt(maxQuantity || '99', 10));
}
}, [rootId]);
// Handle quantity changes with bounds checking.
const handleChange = (newQuantity: number) => {
if (newQuantity < 1 || newQuantity > maxQuantity) return;
setQuantity(newQuantity);
// Update cart via API...
};
return (
<div className="quantity-controls">
<button onClick={() => handleChange(quantity - 1)} disabled={quantity <= 1}>
</button>
<span>{quantity}</span>
<button onClick={() => handleChange(quantity + 1)} disabled={quantity >= maxQuantity}>
+
</button>
</div>
);
}

Type-Safe Data Attributes

Create a utility for reading and parsing data attributes with proper types:

src/utils/data-attributes.ts
/**
* Define how each attribute should be parsed.
* All DOM values are strings - this tells us what type to convert to.
*/
type AttributeType = 'string' | 'number' | 'boolean';
/**
* Schema maps attribute names to their expected types.
* Example: { productId: 'string', quantity: 'number' }
*/
type AttributeSchema = Record<string, AttributeType>;
/**
* Reads data attributes from an element and parses them to correct types.
* @param elementId - The ID of the DOM element to read from.
* @param schema - Object mapping attribute names to their types.
* @returns Parsed attributes object, or null if element not found.
*/
export function readDataAttributes(
elementId: string,
schema: AttributeSchema
): Record<string, unknown> | null {
const element = document.getElementById(elementId);
if (!element) return null;
const result: Record<string, unknown> = {};
for (const [key, type] of Object.entries(schema)) {
// Dataset auto-converts kebab-case to camelCase, but we also check getAttribute.
const dataKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
const value = element.dataset[key] ?? element.getAttribute(`data-${dataKey}`);
// Skip undefined/null attributes.
if (value === undefined || value === null) {
continue;
}
// Parse based on declared type.
switch (type) {
case 'number':
result[key] = parseFloat(value); // Convert "42" to 42.
break;
case 'boolean':
result[key] = value === 'true'; // Convert "true"/"false" to boolean.
break;
default:
result[key] = value; // Keep as string.
}
}
return result;
}

Usage:

// Define schema for expected attributes and their types.
const data = readDataAttributes('quantity-root', {
lineId: 'string',
currentQuantity: 'number',
maxQuantity: 'number',
allowBackorder: 'boolean',
});
if (data) {
// Values are now correctly typed - use type assertions.
const lineId = data.lineId as string;
const quantity = data.currentQuantity as number; // Already a number, not "42"!
const canBackorder = data.allowBackorder as boolean; // true/false, not "true"/"false"!
}

Common Use Cases

1. Product Variant Selection

<div
id="variant-selector-root"
data-product-id="{{ product.id }}"
data-selected-variant-id="{{ product.selected_or_first_available_variant.id }}"
data-option-count="{{ product.options.size }}"
></div>

2. Cart Item Controls

<div
id="cart-item-{{ line_item.key | handle }}"
data-line-key="{{ line_item.key }}"
data-variant-id="{{ line_item.variant_id }}"
data-quantity="{{ line_item.quantity }}"
data-price="{{ line_item.final_line_price }}"
></div>

3. UI State Flags

<div
id="header-root"
data-sticky="{{ section.settings.sticky_header }}"
data-transparent="{{ section.settings.transparent_on_home }}"
data-is-home="{{ template.name == 'index' }}"
></div>

4. Section Settings

<div
id="slideshow-root"
data-autoplay="{{ section.settings.autoplay }}"
data-speed="{{ section.settings.slide_speed }}"
data-show-dots="{{ section.settings.show_dots }}"
data-show-arrows="{{ section.settings.show_arrows }}"
></div>

React Hook for Data Attributes

Create a reusable hook:

src/hooks/useDataAttributes.ts
import { useState, useEffect } from 'react';
/**
* Type for parsing each attribute value.
*/
type AttributeType = 'string' | 'number' | 'boolean';
/**
* Schema mapping attribute names to their expected types.
*/
type AttributeSchema = Record<string, AttributeType>;
/**
* React hook that reads and parses data attributes from a DOM element.
* Returns null initially, then parsed data after mount.
*
* @param elementId - The ID of the element to read attributes from.
* @param schema - Object mapping attribute names to their types.
*/
export function useDataAttributes(
elementId: string,
schema: AttributeSchema
): Record<string, unknown> | null {
const [data, setData] = useState<Record<string, unknown> | null>(null);
// Read attributes after component mounts (DOM is ready).
useEffect(() => {
const element = document.getElementById(elementId);
if (!element) return;
const result: Record<string, unknown> = {};
// Iterate through schema and parse each attribute.
for (const [key, type] of Object.entries(schema)) {
const value = element.dataset[key];
if (value === undefined) continue; // Skip missing attributes.
// Parse based on declared type.
switch (type) {
case 'number':
result[key] = parseFloat(value);
break;
case 'boolean':
result[key] = value === 'true';
break;
default:
result[key] = value;
}
}
setData(result);
}, [elementId]); // Re-run if elementId changes.
return data;
}

Usage in a component:

function Slideshow({ rootId }: { rootId: string }) {
// Use hook with schema to read and parse attributes.
const settings = useDataAttributes(rootId, {
autoplay: 'boolean',
speed: 'number',
showDots: 'boolean',
showArrows: 'boolean',
});
// Settings is null until useEffect runs.
if (!settings) return null;
// Type assertions to access correctly typed values.
const autoplay = settings.autoplay as boolean;
const speed = settings.speed as number;
const showDots = settings.showDots as boolean;
const showArrows = settings.showArrows as boolean;
return (
<div className="slideshow">
{/* Conditionally render based on parsed boolean values */}
{autoplay && <AutoplayController speed={speed} />}
{showDots && <Dots />}
{showArrows && <Arrows />}
</div>
);
}

Best Practices

1. Use Descriptive Names

{%- comment -%} Good {%- endcomment -%}
data-product-id="{{ product.id }}"
data-is-on-sale="{{ product.compare_at_price > product.price }}"
{%- comment -%} Avoid {%- endcomment -%}
data-pid="{{ product.id }}"
data-sale="{{ product.compare_at_price > product.price }}"

2. Escape Strings Properly

{%- comment -%} For simple strings {%- endcomment -%}
data-title="{{ product.title | escape }}"
{%- comment -%} For complex strings that might have special chars {%- endcomment -%}
data-description="{{ product.description | strip_html | escape }}"

3. Handle Missing Values

data-compare-price="{{ product.compare_at_price | default: '' }}"
data-sku="{{ variant.sku | default: 'N/A' }}"

4. Keep It Simple

Data attributes are for simple values. If you find yourself doing this:

{%- comment -%} Don't do this - too complex for data attributes {%- endcomment -%}
data-variants="{{ product.variants | json | escape }}"

Use a JSON script tag instead (covered in the next lesson).

When NOT to Use Data Attributes

ScenarioUse Instead
Complex objects (products, variants)JSON script tags
Arrays with multiple itemsJSON script tags
Data needed by multiple componentsGlobal window object
Large text contentJSON script tags
Nested data structuresJSON script tags

Performance Considerations

Data attributes are extremely fast to read:

// Very fast - direct DOM access via dataset property.
const id = element.dataset.productId;
// Slightly slower but still fast - uses the full attribute name.
const id = element.getAttribute('data-product-id');
// Both approaches have negligible performance impact.

Reading data attributes has negligible performance impact, even with many attributes.

Key Takeaways

  1. Data attributes are best for simple values: IDs, flags, numbers, short strings
  2. Use the dataset API: data-my-value becomes element.dataset.myValue
  3. Values are always strings: Parse numbers and booleans explicitly
  4. Create a typed utility or hook for consistent parsing
  5. Escape values in Liquid: Use | escape for strings
  6. Know when to upgrade: Complex data belongs in JSON script tags

In the next lesson, we’ll explore JSON script tags for passing complex objects like products and variants.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...