The Liquid-React Bridge Intermediate 10 min read

Global Window Objects: Shared State Initialization

Set up global configuration and shared data on the window object for access across all React components. Perfect for shop settings, customer data, and cart state.

Some data needs to be accessible everywhere—shop currency, customer login status, cart count. Rather than threading this through component props or repeating JSON script tags, we can set it on the global window object.

The Window Object Pattern

The browser’s window object is globally accessible from any JavaScript code. We can use Liquid to set data on it before React loads:

{%- comment -%} In layout/theme.liquid, before the React bundle {%- endcomment -%}
<script>
window.Shopify = window.Shopify || {};
window.Shopify.shop = {
name: {{ shop.name | json }},
currency: {{ shop.currency | json }},
moneyFormat: {{ shop.money_format | json }},
locale: {{ request.locale.iso_code | json }}
};
</script>
{%- comment -%} Later: load React bundle {%- endcomment -%}
<script src="{{ 'react-bundle.js' | asset_url }}" defer></script>

Now any React component can access this data:

// Anywhere in your React app - global data is always accessible.
const shopName = window.Shopify?.shop?.name; // Optional chaining for safety.
const currency = window.Shopify?.shop?.currency;
// No need to read from DOM - it's already in memory.

Setting Up Global Data in theme.liquid

Here’s a comprehensive setup for global data:

{%- comment -%} layout/theme.liquid {%- endcomment -%}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{{ 'react-bundle.css' | asset_url | stylesheet_tag }}
{{ content_for_header }}
{%- comment -%} Global Shopify data - available before React loads {%- endcomment -%}
<script>
window.Shopify = window.Shopify || {};
// Shop configuration
window.Shopify.shop = {
name: {{ shop.name | json }},
url: {{ shop.url | json }},
domain: {{ shop.domain | json }},
currency: {{ shop.currency | json }},
moneyFormat: {{ shop.money_format | json }},
moneyWithCurrencyFormat: {{ shop.money_with_currency_format | json }}
};
// Locale settings
window.Shopify.locale = {
code: {{ request.locale.iso_code | json }},
rootUrl: {{ request.locale.root_url | json }},
primary: {{ request.locale.primary | json }}
};
// Customer data (null if not logged in)
window.Shopify.customer = {% if customer %}{
id: {{ customer.id }},
email: {{ customer.email | json }},
firstName: {{ customer.first_name | json }},
lastName: {{ customer.last_name | json }},
name: {{ customer.name | json }},
ordersCount: {{ customer.orders_count }},
totalSpent: {{ customer.total_spent }},
tags: {{ customer.tags | json }}
}{% else %}null{% endif %};
// Cart summary (detailed cart data can use JSON script tag)
window.Shopify.cart = {
itemCount: {{ cart.item_count }},
totalPrice: {{ cart.total_price }},
currency: {{ cart.currency.iso_code | json }}
};
// Current page context
window.Shopify.page = {
template: {{ template.name | json }},
suffix: {{ template.suffix | default: '' | json }}
};
// Routes for API calls
window.Shopify.routes = {
cartUrl: {{ routes.cart_url | json }},
cartAddUrl: {{ routes.cart_add_url | json }},
cartChangeUrl: {{ routes.cart_change_url | json }},
cartUpdateUrl: {{ routes.cart_update_url | json }},
searchUrl: {{ routes.search_url | json }},
predictiveSearchUrl: {{ routes.predictive_search_url | json }}
};
</script>
</head>
<body>
{{ content_for_layout }}
<script src="{{ 'react-bundle.js' | asset_url }}" defer></script>
</body>
</html>

TypeScript Declarations

Extend the Window interface for type safety:

src/types/global.d.ts
/**
* Shop configuration from Liquid's shop object.
*/
interface ShopifyShop {
name: string;
url: string;
domain: string;
currency: string;
moneyFormat: string; // e.g., "${{amount}}"
moneyWithCurrencyFormat: string; // e.g., "${{amount}} USD"
}
/**
* Current locale settings.
*/
interface ShopifyLocale {
code: string; // e.g., "en", "fr"
rootUrl: string; // e.g., "/", "/fr"
primary: boolean; // Is this the primary locale?
}
/**
* Customer data - only populated when logged in.
*/
interface ShopifyCustomer {
id: number;
email: string;
firstName: string;
lastName: string;
name: string; // Full name.
ordersCount: number;
totalSpent: number; // Lifetime value in cents.
tags: string[]; // Customer tags for segmentation.
}
/**
* Cart summary for header display (not full cart data).
*/
interface ShopifyCartSummary {
itemCount: number;
totalPrice: number; // In cents.
currency: string;
}
/**
* Current page context.
*/
interface ShopifyPage {
template: string; // e.g., "product", "collection", "index"
suffix: string; // Template suffix if any.
}
/**
* API route URLs - use these instead of hardcoding paths.
*/
interface ShopifyRoutes {
cartUrl: string;
cartAddUrl: string;
cartChangeUrl: string;
cartUpdateUrl: string;
searchUrl: string;
predictiveSearchUrl: string;
}
/**
* The main Shopify global object structure.
*/
interface ShopifyGlobal {
shop?: ShopifyShop;
locale?: ShopifyLocale;
customer?: ShopifyCustomer | null; // null when logged out.
cart?: ShopifyCartSummary;
page?: ShopifyPage;
routes?: ShopifyRoutes;
}
// Extend the Window interface to include Shopify.
declare global {
interface Window {
Shopify?: ShopifyGlobal;
}
}
// Required for module declaration files.
export {};

Accessing Global Data in React

Direct Access

// Simple access with optional chaining for safety.
const isLoggedIn = !!window.Shopify?.customer; // Double-bang to boolean.
const cartCount = window.Shopify?.cart?.itemCount ?? 0; // Nullish coalescing for default.
const currency = window.Shopify?.shop?.currency ?? 'USD'; // Default to USD.
// Always use ?. and ?? to handle undefined cases gracefully.

Custom Hook

Create a hook for cleaner access:

src/hooks/useShopify.ts
/**
* Main hook for accessing all Shopify global data.
* Provides sensible defaults for all values.
*/
export function useShopify() {
return {
shop: window.Shopify?.shop ?? {
name: '',
currency: 'USD',
moneyFormat: '${{amount}}',
},
customer: window.Shopify?.customer ?? null,
cart: window.Shopify?.cart ?? { itemCount: 0, totalPrice: 0 },
locale: window.Shopify?.locale ?? { code: 'en', rootUrl: '/' },
routes: window.Shopify?.routes ?? {},
page: window.Shopify?.page ?? { template: '', suffix: '' },
};
}
// Convenience hooks for common use cases.
export function useCustomer() {
return window.Shopify?.customer ?? null;
}
export function useIsLoggedIn() {
return !!window.Shopify?.customer; // Convert to boolean.
}
export function useCartCount() {
return window.Shopify?.cart?.itemCount ?? 0;
}
export function useMoneyFormat() {
return window.Shopify?.shop?.moneyFormat ?? '${{amount}}';
}

Usage:

function Header() {
// Destructure shop and cart from the hook.
const { shop, cart } = useShopify();
const isLoggedIn = useIsLoggedIn();
return (
<header>
<h1>{shop.name}</h1>
<nav>
<a href="/cart">Cart ({cart.itemCount})</a>
{/* Conditional rendering based on login status */}
{isLoggedIn ? <a href="/account">My Account</a> : <a href="/account/login">Log In</a>}
</nav>
</header>
);
}

Money Formatting Utility

Use the global money format for consistent currency display:

src/utils/money.ts
/**
* Format cents to a currency string using the shop's money format.
* Supports Shopify's placeholder syntax.
*/
export function formatMoney(cents: number): string {
// Get format from global config, or use USD default.
const format = window.Shopify?.shop?.moneyFormat ?? '${{amount}}';
const amount = (cents / 100).toFixed(2); // Convert cents to dollars.
// Replace Shopify placeholders with actual values.
return format
.replace('{{amount}}', amount)
.replace('{{amount_no_decimals}}', Math.round(cents / 100).toString())
.replace('{{amount_with_comma_separator}}', amount.replace('.', ','));
}
/**
* Format with currency code (e.g., "$19.99 USD").
*/
export function formatMoneyWithCurrency(cents: number): string {
const format = window.Shopify?.shop?.moneyWithCurrencyFormat ?? '${{amount}} USD';
const amount = (cents / 100).toFixed(2);
return format
.replace('{{amount}}', amount)
.replace('{{amount_no_decimals}}', Math.round(cents / 100).toString());
}

Updating Global State

The global object can also be updated by React (e.g., after a cart change):

src/stores/cart.ts
import { create } from 'zustand';
interface CartState {
itemCount: number;
totalPrice: number;
// ... other cart data
updateFromApi: (apiResponse: CartApiResponse) => void;
}
export const useCartStore = create<CartState>((set) => ({
// Initialize from global object (set by Liquid).
itemCount: window.Shopify?.cart?.itemCount ?? 0,
totalPrice: window.Shopify?.cart?.totalPrice ?? 0,
// Update both Zustand state and global object.
updateFromApi: (response) => {
// Update Zustand store for React components.
set({
itemCount: response.item_count,
totalPrice: response.total_price,
});
// Also update global object for non-React code.
// This keeps Liquid-rendered elements in sync.
if (window.Shopify?.cart) {
window.Shopify.cart.itemCount = response.item_count;
window.Shopify.cart.totalPrice = response.total_price;
}
},
}));

Page-Specific Data

For data that varies by page, combine with JSON script tags:

{%- comment -%} Global data in theme.liquid {%- endcomment -%}
<script>
window.Shopify = { /* ... */ };
</script>
{%- comment -%} Page-specific data in the template/section {%- endcomment -%}
{%- if template.name == 'product' -%}
<script type="application/json" id="product-data">
{{ product | json }}
</script>
{%- endif -%}

React components read global data from window.Shopify and page-specific data from JSON script tags.

Common Use Cases

1. Conditional Rendering Based on Customer

function WishlistButton({ productId }: { productId: number }) {
const customer = useCustomer();
// Show different UI based on login status.
if (!customer) {
return <a href="/account/login">Login to Save</a>;
}
return <button onClick={() => addToWishlist(productId)}>Save</button>;
}

2. Currency-Aware Pricing

function Price({ cents }: { cents: number }) {
return <span className="price">{formatMoney(cents)}</span>;
}

3. Template-Specific Behavior

function SearchBar() {
const { page } = useShopify();
// Conditional render based on current template.
if (page.template === 'search') {
return null; // Don't show search on search results page.
}
return <SearchInput />;
}

4. API Routes

async function addToCart(variantId: number, quantity: number) {
// Use route from global config - don't hardcode URLs.
const addUrl = window.Shopify?.routes?.cartAddUrl ?? '/cart/add.js';
const response = await fetch(addUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: variantId, quantity }),
});
return response.json();
}

Best Practices

1. Set Globals Before React Loads

Place the script in <head> or before the React bundle:

<script>window.Shopify = { /* ... */ };</script>
<script src="{{ 'react-bundle.js' | asset_url }}" defer></script>

2. Use Optional Chaining

Always access with ?. to handle undefined cases:

// Safe access - always use optional chaining.
const name = window.Shopify?.shop?.name ?? 'Shop';
// Not safe - will throw if any property is undefined!
const name = window.Shopify.shop.name; // TypeError if Shopify or shop is undefined.

3. Provide Defaults

const cartCount = window.Shopify?.cart?.itemCount ?? 0;
const currency = window.Shopify?.shop?.currency ?? 'USD';

4. Keep It Minimal

Only put truly global data on window. Page-specific data belongs in JSON script tags.

Key Takeaways

  1. Window object is globally accessible—perfect for shop-wide configuration
  2. Set data before React loads in theme.liquid
  3. Use TypeScript declarations for type safety with declare global
  4. Create custom hooks for clean access patterns
  5. Combine with JSON script tags for page-specific data
  6. Always use optional chaining when accessing global data
  7. Update both Zustand and window when cart changes (for non-React code)

In the next lesson, we’ll build a unified data bridge utility that combines all these patterns into a single, type-safe API.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...