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:
import { createRoot } from 'react-dom/client';
// Import all componentsimport { Header } from './components/header/Header';import { ProductForm } from './components/product/ProductForm';import { CartDrawer } from './components/cart/CartDrawer';import { SearchModal } from './components/search/SearchModal';
// Import stylesimport './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 readyif (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:
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); }}
// Initializeif (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:
/** * 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:
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:
import { lazy, Suspense } from 'react';import { createRoot } from 'react-dom/client';
// Lazy load heavy componentsconst MediaGallery = lazy(() => import('./components/product/MediaGallery'));const FilterSidebar = lazy(() => import('./components/collection/FilterSidebar'));
// Eagerly load critical componentsimport { 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:
/** * 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 eventsdocument.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:
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?
| Scenario | Recommended Strategy |
|---|---|
| Small theme, few components | Single entry point |
| Large theme, many page types | Multiple entry points |
| Heavy components on specific pages | Lazy loading |
| Need shared state across components | Global providers |
| Theme editor compatibility | Section reload handling |
Key Takeaways
- Single entry point is simplest—one bundle, one script tag
- Component registry provides clean organization and dynamic mounting
- Props from DOM bridge Liquid data to React components
- Multiple entry points reduce page-specific bundle sizes
- Lazy loading splits bundles within a single entry point
- Section reload handling ensures theme editor compatibility
- 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...