React Foundation in Theme Context Intermediate 12 min read

Entry Points and Mount Strategies

Learn how to configure React entry points and implement different mounting strategies for Shopify theme components. Understand when to use single vs multiple entry points.

Unlike a traditional React SPA where you mount one root component, a Shopify theme needs React to mount on multiple, independent elements across different pages. In this lesson, we’ll explore strategies for managing these entry points.

The Single Entry Point Pattern

The simplest approach is one main entry file that handles all component mounting:

src/main.tsx
import { createRoot } from 'react-dom/client';
// Import all components
import { Header } from './components/header/Header';
import { ProductForm } from './components/product/ProductForm';
import { CartDrawer } from './components/cart/CartDrawer';
import { SearchModal } from './components/search/SearchModal';
// Import styles
import './styles/main.css';
/**
* Registry mapping element IDs to React components
*/
const COMPONENT_REGISTRY: Record<string, React.ComponentType> = {
'header-root': Header,
'product-form-root': ProductForm,
'cart-drawer-root': CartDrawer,
'search-modal-root': SearchModal,
};
/**
* Mounts React components to their designated DOM elements
*/
function mountComponents() {
for (const [elementId, Component] of Object.entries(COMPONENT_REGISTRY)) {
const element = document.getElementById(elementId);
if (element) {
const root = createRoot(element);
root.render(<Component />);
}
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountComponents);
} else {
mountComponents();
}

Pros of Single Entry Point

  • Simple bundling: One output file, one script tag
  • Shared code: Components share React, utilities, and state
  • Easy debugging: All React code in one place

Cons of Single Entry Point

  • Larger initial bundle: User downloads all components even if not needed
  • No page-specific loading: Can’t load cart components only on cart page

The Component Registry Pattern

Let’s improve the registry with better organization and error handling:

src/main.tsx
import { StrictMode } from 'react';
import { createRoot, Root } from 'react-dom/client';
import './styles/main.css';
/**
* Tracks mounted React roots for cleanup
*/
const mountedRoots: Map<string, Root> = new Map();
/**
* Component configuration with lazy loading support
*/
interface ComponentConfig {
component: React.ComponentType;
wrapper?: React.ComponentType<{ children: React.ReactNode }>;
}
/**
* Registry of mountable components
*/
const COMPONENTS: Record<string, ComponentConfig> = {
'header-root': {
component: require('./components/header/Header').Header,
},
'product-form-root': {
component: require('./components/product/ProductForm').ProductForm,
},
'cart-drawer-root': {
component: require('./components/cart/CartDrawer').CartDrawer,
},
'search-modal-root': {
component: require('./components/search/SearchModal').SearchModal,
},
};
/**
* Mounts a single component to an element
*/
function mountComponent(elementId: string, config: ComponentConfig): void {
const element = document.getElementById(elementId);
if (!element) return;
// Skip if already mounted
if (mountedRoots.has(elementId)) {
console.warn(`Component already mounted on #${elementId}`);
return;
}
const { component: Component, wrapper: Wrapper } = config;
const root = createRoot(element);
const content = Wrapper ? (
<Wrapper>
<Component />
</Wrapper>
) : (
<Component />
);
root.render(<StrictMode>{content}</StrictMode>);
mountedRoots.set(elementId, root);
}
/**
* Unmounts a component and cleans up
*/
function unmountComponent(elementId: string): void {
const root = mountedRoots.get(elementId);
if (root) {
root.unmount();
mountedRoots.delete(elementId);
}
}
/**
* Mounts all registered components
*/
function mountAllComponents(): void {
for (const [elementId, config] of Object.entries(COMPONENTS)) {
mountComponent(elementId, config);
}
}
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountAllComponents);
} else {
mountAllComponents();
}
// Expose for dynamic mounting (useful for Shopify sections)
window.ReactTheme = {
mount: mountComponent,
unmount: unmountComponent,
mountAll: mountAllComponents,
};

Passing Props from the DOM

Components often need data from the mount point. Here’s how to pass props:

src/main.tsx
/**
* Reads data attributes from an element as props
*/
function getPropsFromElement(element: HTMLElement): Record<string, unknown> {
const props: Record<string, unknown> = {};
// Read all data-* attributes
for (const [key, value] of Object.entries(element.dataset)) {
// Try to parse as JSON, fall back to string
try {
props[key] = JSON.parse(value);
} catch {
props[key] = value;
}
}
return props;
}
function mountComponent(elementId: string, config: ComponentConfig): void {
const element = document.getElementById(elementId);
if (!element) return;
const { component: Component } = config;
const props = getPropsFromElement(element);
const root = createRoot(element);
root.render(
<StrictMode>
<Component {...props} />
</StrictMode>
);
mountedRoots.set(elementId, root);
}

Liquid template:

<div
id="product-form-root"
data-product-id="{{ product.id }}"
data-show-quantity="true"
data-initial-variant="{{ product.selected_or_first_available_variant.id }}"
></div>

React component receives:

interface ProductFormProps {
productId: string;
showQuantity: boolean;
initialVariant: string;
}
function ProductForm({ productId, showQuantity, initialVariant }: ProductFormProps) {
// Props are automatically parsed from data attributes
}

Multiple Entry Points Pattern

For larger themes, split your bundle by page type:

src/
├── entries/
│ ├── main.tsx # Shared components (header, cart drawer)
│ ├── product.tsx # Product page only
│ ├── collection.tsx # Collection page only
│ └── cart.tsx # Cart page only
└── components/

Vite configuration:

vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'shopify-theme/assets',
emptyOutDir: false,
rollupOptions: {
input: {
main: resolve(__dirname, 'src/entries/main.tsx'),
product: resolve(__dirname, 'src/entries/product.tsx'),
collection: resolve(__dirname, 'src/entries/collection.tsx'),
},
output: {
entryFileNames: 'react-[name].js',
chunkFileNames: 'react-chunk-[name].js',
assetFileNames: 'react-[name].[ext]',
},
},
},
});

Conditional loading in Liquid:

{%- comment -%} layout/theme.liquid {%- endcomment -%}
{%- comment -%} Always load shared components {%- endcomment -%}
<script src="{{ 'react-main.js' | asset_url }}" defer></script>
{%- comment -%} Load page-specific bundles {%- endcomment -%}
{%- case template.name -%}
{%- when 'product' -%}
<script src="{{ 'react-product.js' | asset_url }}" defer></script>
{%- when 'collection' -%}
<script src="{{ 'react-collection.js' | asset_url }}" defer></script>
{%- endcase -%}

Lazy Loading Components

For the single entry point approach, use React’s lazy loading to code-split:

src/main.tsx
import { lazy, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
// Lazy load heavy components
const MediaGallery = lazy(() => import('./components/product/MediaGallery'));
const FilterSidebar = lazy(() => import('./components/collection/FilterSidebar'));
// Eagerly load critical components
import { Header } from './components/header/Header';
import { CartDrawer } from './components/cart/CartDrawer';
/**
* Loading fallback for lazy components
*/
function LoadingFallback() {
return <div className="react-loading">Loading...</div>;
}
/**
* Wraps lazy components with Suspense
*/
function withSuspense(Component: React.LazyExoticComponent<React.ComponentType>) {
return function SuspenseWrapper(props: Record<string, unknown>) {
return (
<Suspense fallback={<LoadingFallback />}>
<Component {...props} />
</Suspense>
);
};
}
const COMPONENTS: Record<string, React.ComponentType> = {
// Critical - loaded immediately
'header-root': Header,
'cart-drawer-root': CartDrawer,
// Lazy - loaded on demand
'media-gallery-root': withSuspense(MediaGallery),
'filter-sidebar-root': withSuspense(FilterSidebar),
};

Handling Section Re-renders

Shopify’s theme editor can reload sections dynamically. Handle this by listening for section events:

src/main.tsx
/**
* Handles Shopify theme editor section reload
*/
function handleSectionReload(event: Event): void {
const customEvent = event as CustomEvent<{ sectionId: string }>;
const sectionId = customEvent.detail?.sectionId;
if (!sectionId) return;
// Find any React roots in this section and re-mount
const section = document.getElementById(`shopify-section-${sectionId}`);
if (!section) return;
// Find all mount points in this section
for (const elementId of Object.keys(COMPONENTS)) {
const element = section.querySelector(`#${elementId}`);
if (element) {
// Unmount old root if exists
unmountComponent(elementId);
// Mount fresh component
mountComponent(elementId, COMPONENTS[elementId]);
}
}
}
// Listen for Shopify theme editor events
document.addEventListener('shopify:section:load', handleSectionReload);
document.addEventListener('shopify:section:reorder', handleSectionReload);

Global Providers Pattern

When using state management or context, wrap all components in shared providers:

src/main.tsx
import { CartProvider } from './context/CartContext';
import { UIProvider } from './context/UIContext';
/**
* Global providers wrapper
*/
function GlobalProviders({ children }: { children: React.ReactNode }) {
return (
<CartProvider>
<UIProvider>{children}</UIProvider>
</CartProvider>
);
}
function mountComponent(elementId: string, config: ComponentConfig): void {
const element = document.getElementById(elementId);
if (!element) return;
const { component: Component } = config;
const props = getPropsFromElement(element);
const root = createRoot(element);
root.render(
<StrictMode>
<GlobalProviders>
<Component {...props} />
</GlobalProviders>
</StrictMode>
);
mountedRoots.set(elementId, root);
}

Which Strategy to Choose?

ScenarioRecommended Strategy
Small theme, few componentsSingle entry point
Large theme, many page typesMultiple entry points
Heavy components on specific pagesLazy loading
Need shared state across componentsGlobal providers
Theme editor compatibilitySection reload handling

Key Takeaways

  1. Single entry point is simplest—one bundle, one script tag
  2. Component registry provides clean organization and dynamic mounting
  3. Props from DOM bridge Liquid data to React components
  4. Multiple entry points reduce page-specific bundle sizes
  5. Lazy loading splits bundles within a single entry point
  6. Section reload handling ensures theme editor compatibility
  7. Global providers share state across independently mounted components

In the next lesson, we’ll design the component architecture that works best in a Shopify theme context.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...