Build, Deploy, and Ship Advanced 20 min read

Final Capstone: Complete Theme Walkthrough

Bring together everything you have learned to build a complete React Shopify theme. Walk through the architecture, key components, and deployment of a production-ready hybrid theme.

Congratulations on reaching the final lesson. You’ve learned to bridge Liquid and React, build interactive components, manage state, handle testing, optimize performance, and deploy with confidence. Now let’s bring it all together in a complete theme walkthrough.

The Complete Architecture

Here’s how all the pieces fit together:

┌─────────────────────────────────────────────────────────────────────────┐
│ REACT SHOPIFY THEME ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ SHOPIFY REACT │
│ ┌────────────────────┐ ┌────────────────────────────────┐ │
│ │ │ │ │ │
│ │ Liquid Templates │──────────▶│ React Components │ │
│ │ (Data Layer) │ JSON │ (UI Layer) │ │
│ │ │ Bridge │ │ │
│ │ • templates/ │ │ • ProductPage │ │
│ │ • sections/ │ │ • CollectionGrid │ │
│ │ • snippets/ │ │ • CartDrawer │ │
│ │ │ │ • SearchModal │ │
│ └────────────────────┘ │ • Header/Navigation │ │
│ │ │ │ │
│ │ └─────────────┬──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────┐ ┌────────────────────────────────┐ │
│ │ │ │ │ │
│ │ Shopify APIs │◀─────────▶│ State Management │ │
│ │ │ AJAX │ │ │
│ │ • Cart API │ │ • Cart Store (Zustand) │ │
│ │ • Storefront API │ │ • UI State │ │
│ │ • Admin API │ │ • Product State │ │
│ │ │ │ │ │
│ └────────────────────┘ └────────────────────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────────── │
│ │
│ BUILD PIPELINE DEPLOYMENT │
│ ┌────────────────────┐ ┌────────────────────────────────┐ │
│ │ │ │ │ │
│ │ Vite Build │──────────▶│ GitHub Actions │ │
│ │ • TypeScript │ │ • CI/CD Pipeline │ │
│ │ • Code Splitting │ │ • Theme Check │ │
│ │ • Minification │ │ • Shopify CLI Deploy │ │
│ │ │ │ │ │
│ └────────────────────┘ └────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Project Structure

A production theme organizes code for maintainability:

shopify-react-theme/
├── .github/
│ └── workflows/
│ ├── ci.yml # Build and test
│ ├── deploy.yml # Deploy to Shopify
│ └── theme-check.yml # Liquid linting
├── src/ # React source code
│ ├── components/
│ │ ├── cart/
│ │ │ ├── CartDrawer/
│ │ │ ├── CartItem/
│ │ │ └── CartTotals/
│ │ ├── layout/
│ │ │ ├── Header/
│ │ │ ├── Footer/
│ │ │ └── Navigation/
│ │ ├── product/
│ │ │ ├── ProductPage/
│ │ │ ├── ProductGallery/
│ │ │ ├── VariantSelector/
│ │ │ └── AddToCartButton/
│ │ ├── collection/
│ │ │ ├── ProductGrid/
│ │ │ ├── ProductCard/
│ │ │ └── Filters/
│ │ ├── search/
│ │ │ └── SearchModal/
│ │ └── ui/
│ │ ├── Button/
│ │ ├── Input/
│ │ ├── Modal/
│ │ └── Skeleton/
│ │
│ ├── hooks/
│ │ ├── useCart.ts
│ │ ├── useProduct.ts
│ │ ├── useSearch.ts
│ │ └── useMediaQuery.ts
│ │
│ ├── lib/
│ │ ├── shopify-api.ts
│ │ ├── bridge.ts
│ │ ├── money.ts
│ │ └── analytics.ts
│ │
│ ├── stores/
│ │ ├── cart.ts
│ │ └── ui.ts
│ │
│ ├── styles/
│ │ ├── critical.css
│ │ └── theme.css
│ │
│ ├── types/
│ │ └── shopify.ts
│ │
│ └── entries/
│ ├── main.tsx
│ ├── product.tsx
│ ├── collection.tsx
│ └── cart.tsx
├── theme/ # Shopify theme files
│ ├── assets/ # Built React + theme assets
│ ├── config/
│ ├── layout/
│ │ └── theme.liquid
│ ├── sections/
│ │ ├── header.liquid
│ │ ├── product-main.liquid
│ │ ├── collection-grid.liquid
│ │ └── cart-drawer.liquid
│ ├── snippets/
│ │ ├── bridge-data.liquid
│ │ ├── critical-css.liquid
│ │ └── asset-manifest.liquid
│ └── templates/
│ ├── product.json
│ ├── collection.json
│ └── cart.json
├── scripts/
│ ├── copy-to-theme.ts
│ ├── generate-manifest.ts
│ └── validate-build.ts
├── vite.config.ts
├── tsconfig.json
├── package.json
└── README.md

The Data Bridge Pattern

The bridge connects Liquid data to React components:

{% comment %} sections/product-main.liquid {% endcomment %}
<div id="product-root" data-section-id="{{ section.id }}"></div>
<script type="application/json" id="product-data-{{ section.id }}">
{
"product": {
"id": {{ product.id | json }},
"title": {{ product.title | json }},
"handle": {{ product.handle | json }},
"description": {{ product.description | json }},
"vendor": {{ product.vendor | json }},
"images": [
{%- for image in product.images -%}
{
"id": {{ image.id | json }},
"src": {{ image.src | image_url: width: 1200 | json }},
"alt": {{ image.alt | json }},
"width": {{ image.width }},
"height": {{ image.height }}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
],
"variants": [
{%- for variant in product.variants -%}
{
"id": {{ variant.id | json }},
"title": {{ variant.title | json }},
"price": {{ variant.price }},
"compareAtPrice": {{ variant.compare_at_price | default: 'null' }},
"available": {{ variant.available }},
"options": {{ variant.options | json }},
"image": {% if variant.image %}{{ variant.image.src | image_url: width: 800 | json }}{% else %}null{% endif %}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
],
"options": {{ product.options_with_values | json }}
},
"selectedVariantId": {{ product.selected_or_first_available_variant.id | json }},
"settings": {
"showQuantity": {{ section.settings.show_quantity | json }},
"enableZoom": {{ section.settings.enable_zoom | json }},
"stickyAddToCart": {{ section.settings.sticky_atc | json }}
}
}
</script>
{% schema %}
{
"name": "Product Main",
"settings": [
{
"type": "checkbox",
"id": "show_quantity",
"label": "Show quantity selector",
"default": true
},
{
"type": "checkbox",
"id": "enable_zoom",
"label": "Enable image zoom",
"default": true
},
{
"type": "checkbox",
"id": "sticky_atc",
"label": "Sticky add to cart on mobile",
"default": true
}
]
}
{% endschema %}

React mounts and reads this data:

src/entries/product.tsx
import { createRoot } from 'react-dom/client';
import { ProductPage } from '@/components/product/ProductPage';
import { parseBridgeData } from '@/lib/bridge';
import type { ProductPageData } from '@/types/shopify';
function mountProductPage() {
const roots = document.querySelectorAll('[id^="product-root"]');
roots.forEach((root) => {
const sectionId = root.getAttribute('data-section-id');
const scriptId = `product-data-${sectionId}`;
const result = parseBridgeData<ProductPageData>(scriptId, 'ProductPage');
if (result.success && result.data) {
createRoot(root).render(
<ProductPage
product={result.data.product}
selectedVariantId={result.data.selectedVariantId}
settings={result.data.settings}
/>
);
}
});
}
// Mount when ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountProductPage);
} else {
mountProductPage();
}

State Management

The cart store manages global cart state:

src/stores/cart.ts
import { create } from 'zustand';
import { cartApi } from '@/lib/shopify-api';
import type { Cart, CartItem } from '@/types/shopify';
interface CartState {
cart: Cart | null;
isLoading: boolean;
isOpen: boolean;
error: string | null;
// Actions
fetchCart: () => Promise<void>;
addItem: (variantId: string, quantity: number) => Promise<void>;
updateItem: (lineId: string, quantity: number) => Promise<void>;
removeItem: (lineId: string) => Promise<void>;
openCart: () => void;
closeCart: () => void;
}
export const useCartStore = create<CartState>((set, get) => ({
cart: null,
isLoading: false,
isOpen: false,
error: null,
fetchCart: async () => {
set({ isLoading: true, error: null });
try {
const cart = await cartApi.get();
set({ cart, isLoading: false });
} catch (error) {
set({ error: 'Failed to load cart', isLoading: false });
}
},
addItem: async (variantId, quantity) => {
const previousCart = get().cart;
// Optimistic update
set((state) => ({
cart: state.cart ? {
...state.cart,
itemCount: state.cart.itemCount + quantity,
} : null,
}));
try {
const updatedCart = await cartApi.add(variantId, quantity);
set({ cart: updatedCart, isOpen: true });
} catch (error) {
// Rollback
set({ cart: previousCart, error: 'Failed to add item' });
throw error;
}
},
updateItem: async (lineId, quantity) => {
const previousCart = get().cart;
// Optimistic update
set((state) => ({
cart: state.cart ? {
...state.cart,
items: state.cart.items.map((item) =>
item.id === lineId ? { ...item, quantity } : item
),
} : null,
}));
try {
const updatedCart = await cartApi.update(lineId, quantity);
set({ cart: updatedCart });
} catch (error) {
set({ cart: previousCart, error: 'Failed to update item' });
throw error;
}
},
removeItem: async (lineId) => {
const previousCart = get().cart;
// Optimistic update
set((state) => ({
cart: state.cart ? {
...state.cart,
items: state.cart.items.filter((item) => item.id !== lineId),
} : null,
}));
try {
const updatedCart = await cartApi.remove(lineId);
set({ cart: updatedCart });
} catch (error) {
set({ cart: previousCart, error: 'Failed to remove item' });
throw error;
}
},
openCart: () => set({ isOpen: true }),
closeCart: () => set({ isOpen: false }),
}));

Component Patterns

A well-structured component example:

src/components/product/AddToCartButton/AddToCartButton.tsx
import { useState, useCallback } from 'react';
import { useCartStore } from '@/stores/cart';
import { Button } from '@/components/ui/Button';
import { Spinner } from '@/components/ui/Spinner';
import { CheckIcon } from '@/components/icons';
import styles from './AddToCartButton.module.css';
type ButtonState = 'idle' | 'loading' | 'success' | 'error';
interface AddToCartButtonProps {
variantId: string;
quantity?: number;
available?: boolean;
className?: string;
}
export function AddToCartButton({
variantId,
quantity = 1,
available = true,
className,
}: AddToCartButtonProps) {
const [state, setState] = useState<ButtonState>('idle');
const addItem = useCartStore((s) => s.addItem);
const handleClick = useCallback(async () => {
if (state === 'loading' || !available) return;
setState('loading');
try {
await addItem(variantId, quantity);
setState('success');
setTimeout(() => setState('idle'), 2000);
} catch (error) {
setState('error');
setTimeout(() => setState('idle'), 3000);
}
}, [variantId, quantity, available, addItem, state]);
const getButtonText = () => {
switch (state) {
case 'loading': return 'Adding...';
case 'success': return 'Added!';
case 'error': return 'Try Again';
default: return available ? 'Add to Cart' : 'Sold Out';
}
};
return (
<Button
onClick={handleClick}
disabled={!available || state === 'loading'}
className={`${styles.button} ${styles[state]} ${className || ''}`}
aria-busy={state === 'loading'}
>
{state === 'loading' && <Spinner size={18} />}
{state === 'success' && <CheckIcon />}
<span>{getButtonText()}</span>
</Button>
);
}

Performance Optimizations Applied

The theme implements all performance best practices:

// Code splitting - lazy load non-critical components
const ProductZoom = lazy(() => import('./ProductZoom'));
const ProductReviews = lazy(() => import('./ProductReviews'));
// Memoization - prevent unnecessary re-renders
const ProductCard = memo(function ProductCard({ product }: Props) {
// Component implementation
});
// Virtualization - render only visible items
function ProductGrid({ products }: { products: Product[] }) {
const { ref, inView } = useInView({ threshold: 0 });
return (
<div ref={ref}>
{products.map((product, index) => (
<ProductCard
key={product.id}
product={product}
// Only fully render when in view
priority={index < 4}
/>
))}
</div>
);
}

Testing Strategy

Tests cover critical paths:

src/components/product/AddToCartButton/AddToCartButton.test.tsx
describe('AddToCartButton', () => {
it('adds item to cart when clicked', async () => {
const user = userEvent.setup();
const addItem = vi.fn().mockResolvedValue(undefined);
vi.mocked(useCartStore).mockReturnValue({ addItem });
render(<AddToCartButton variantId="123" />);
await user.click(screen.getByRole('button'));
expect(addItem).toHaveBeenCalledWith('123', 1);
});
it('shows loading then success state', async () => {
const user = userEvent.setup();
const addItem = vi.fn().mockResolvedValue(undefined);
vi.mocked(useCartStore).mockReturnValue({ addItem });
render(<AddToCartButton variantId="123" />);
await user.click(screen.getByRole('button'));
expect(screen.getByText('Adding...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Added!')).toBeInTheDocument();
});
});
it('is disabled when sold out', () => {
render(<AddToCartButton variantId="123" available={false} />);
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByText('Sold Out')).toBeInTheDocument();
});
});

Deployment Flow

The complete deployment process:

# Simplified deployment workflow
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install, Test, Build
run: |
npm ci
npm run type-check
npm run lint
npm run test:run
npm run build
- name: Deploy to Shopify
run: |
shopify theme push \
--store=${{ secrets.SHOPIFY_STORE_URL }} \
--theme=${{ secrets.SHOPIFY_THEME_ID }} \
--path=theme \
--allow-live
env:
SHOPIFY_CLI_THEME_TOKEN: ${{ secrets.SHOPIFY_ACCESS_TOKEN }}

What You’ve Accomplished

Through this course, you’ve built expertise in:

┌─────────────────────────────────────────────────────────────────┐
│ SKILLS MASTERED │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Architecture │
│ ✓ Hybrid Liquid-React architecture │
│ ✓ Data bridge patterns │
│ ✓ Component organization │
│ │
│ Development │
│ ✓ TypeScript for type safety │
│ ✓ Vite for modern tooling │
│ ✓ CSS Modules for styling │
│ │
│ State Management │
│ ✓ Zustand for global state │
│ ✓ Optimistic updates │
│ ✓ Error handling │
│ │
│ Components │
│ ✓ Product pages │
│ ✓ Collection grids │
│ ✓ Cart functionality │
│ ✓ Search and navigation │
│ │
│ Testing │
│ ✓ Unit tests with Vitest │
│ ✓ Integration tests with MSW │
│ ✓ Debugging techniques │
│ │
│ Performance │
│ ✓ Code splitting │
│ ✓ Bundle optimization │
│ ✓ Critical CSS │
│ ✓ Core Web Vitals monitoring │
│ │
│ Deployment │
│ ✓ Production builds │
│ ✓ CI/CD with GitHub Actions │
│ ✓ Cache management │
│ │
└─────────────────────────────────────────────────────────────────┘

Next Steps

Your journey doesn’t end here. Consider these paths forward:

  1. Add more interactivity: Implement quick view modals, wishlist functionality, or product comparison.

  2. Explore the Storefront API: For more complex data needs, integrate GraphQL queries.

  3. Build custom apps: Use Shopify’s App Bridge to create embedded admin experiences.

  4. Contribute to the community: Share your theme components, write about your learnings.

  5. Stay current: Follow Shopify’s changelog and React’s development.

Final Thoughts

Building a React Shopify theme is challenging—you’re bridging two very different paradigms. But the result is powerful: the SEO benefits and server rendering of Liquid combined with the interactivity and developer experience of React.

Remember the principles that guide this architecture:

  • Liquid owns the data, React owns the UI
  • Progressive enhancement—the page works without JavaScript
  • Performance is a feature—measure everything
  • Test the critical paths—users depend on them
  • Automate deployment—humans make mistakes

Thank you for completing this course. Now go build something amazing.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...