Introduction and Architecture Overview Intermediate 10 min read

Choosing Your Stack: Vite vs Webpack, TypeScript, State Management

Compare build tools, type systems, and state management solutions for your React-powered Shopify theme. Make informed decisions about your development stack.

Before we write any code, we need to make some technology decisions. The tools you choose will affect your development experience, build times, and final bundle size. Let’s compare the options and make informed choices.

Build Tools: Vite vs Webpack

Both tools can compile React and output bundles for your Shopify theme. Here’s how they compare:

Vite: The Modern Choice

Vite (French for “fast”) is a next-generation build tool that prioritizes developer experience:

Terminal window
# Development server starts in milliseconds
npm run dev
# Hot Module Replacement is instant
# No waiting for full rebuilds

Vite Advantages:

FeatureBenefit
Native ES modulesInstant server start, no bundling needed in dev
Hot Module ReplacementSub-second updates when you save files
Rollup-based production buildsOptimized, tree-shaken output
First-class TypeScript supportNo additional configuration needed
Simple configurationLess boilerplate than Webpack

Basic Vite Config for Shopify:

vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'assets',
rollupOptions: {
input: 'src/main.tsx',
output: {
entryFileNames: 'react-bundle.js',
assetFileNames: 'react-bundle.[ext]',
},
},
// Don't hash filenames - Shopify handles caching
manifest: false,
},
});

Webpack: The Established Standard

Webpack has been the standard for years and powers Shopify’s Dawn theme:

webpack.config.js
const path = require('path');
module.exports = {
entry: './src/main.tsx',
output: {
filename: 'react-bundle.js',
path: path.resolve(__dirname, 'assets'),
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
};

Webpack Advantages:

FeatureBenefit
Mature ecosystemPlugin for nearly anything
Battle-testedPredictable behavior in edge cases
Dawn theme compatibilityEasy integration with existing Dawn builds
More controlFine-grained configuration options

Which Should You Choose?

Choose Vite if:

  • Starting a new project
  • Developer experience is a priority
  • You want fast iteration cycles
  • You prefer convention over configuration

Choose Webpack if:

  • Extending an existing Dawn-based theme
  • You need specific Webpack plugins
  • Your team already knows Webpack well
  • You need maximum configuration control

This Course: We’ll use Vite as our primary tool with Webpack configuration covered as an alternative in Module 2.

TypeScript adds static typing to JavaScript. For a React-Shopify theme, it’s particularly valuable:

Why TypeScript for Shopify Themes?

  1. Shopify Object Types: Define interfaces for products, variants, and cart items
  2. Catch Errors Early: Find bugs at build time, not in production
  3. Better IDE Support: Autocomplete and inline documentation
  4. Refactoring Confidence: Rename properties safely across your codebase

Defining Shopify Types

src/types/shopify.ts
export interface ShopifyImage {
src: string;
alt: string | null;
width: number;
height: number;
}
export interface ShopifyVariant {
id: number;
title: string;
price: number;
compare_at_price: number | null;
available: boolean;
options: string[];
featured_image: ShopifyImage | null;
}
export interface ShopifyProduct {
id: number;
title: string;
handle: string;
description: string;
available: boolean;
variants: ShopifyVariant[];
images: ShopifyImage[];
options: {
name: string;
position: number;
values: string[];
}[];
}
export interface ShopifyCartItem {
id: number;
quantity: number;
title: string;
price: number;
line_price: number;
variant_id: number;
product_id: number;
image: string;
handle: string;
variant_title: string | null;
}
export interface ShopifyCart {
token: string;
note: string | null;
attributes: Record<string, string>;
item_count: number;
total_price: number;
items: ShopifyCartItem[];
}

Using Types with Liquid Data

src/utils/data-bridge.ts
import type { ShopifyProduct, ShopifyCart } from '../types/shopify';
/*
* Data Bridge: How React reads Liquid data
* Liquid renders JSON into <script type="application/json"> tags.
* React reads and parses these to get initial data without API calls.
*/
export function getProductData(elementId: string): ShopifyProduct | null {
// Find the JSON script tag rendered by Liquid
const element = document.getElementById(elementId);
if (!element?.textContent) return null;
try {
// Parse JSON and cast to our TypeScript type for type safety
return JSON.parse(element.textContent) as ShopifyProduct;
} catch {
// Handle malformed JSON gracefully
console.error(`Failed to parse product data from #${elementId}`);
return null;
}
}
export function getCartData(): ShopifyCart | null {
const element = document.getElementById('cart-data');
if (!element?.textContent) return null;
try {
return JSON.parse(element.textContent) as ShopifyCart;
} catch {
console.error('Failed to parse cart data');
return null;
}
}

TypeScript Configuration

tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

State Management: Choosing Your Solution

React’s built-in useState and useContext work fine for simple cases, but a Shopify theme needs shared state for the cart, UI drawers, and more.

Zustand is a small, fast, and unopinionated state management library:

src/stores/cart.ts
import { create } from 'zustand';
import type { ShopifyCart, ShopifyCartItem } from '../types/shopify';
/*
* Zustand Store Pattern
* Combines state and actions in a single object. Components subscribe to
* only the state they need, minimizing re-renders.
*/
interface CartState {
// State
cart: ShopifyCart | null;
isOpen: boolean; // Cart drawer visibility
isLoading: boolean; // Shows loading spinners during API calls
// Actions - functions that modify state
setCart: (cart: ShopifyCart) => void;
addItem: (variantId: number, quantity: number) => Promise<void>;
updateItem: (lineId: number, quantity: number) => Promise<void>;
removeItem: (lineId: number) => Promise<void>;
toggleDrawer: () => void;
}
// create() returns a hook - components call useCart() to access state
export const useCart = create<CartState>((set, get) => ({
// Initial state
cart: null,
isOpen: false,
isLoading: false,
// Simple setter - just updates cart state
setCart: (cart) => set({ cart }),
// Async action: add item via Shopify AJAX API
addItem: async (variantId, quantity) => {
set({ isLoading: true }); // Show loading state
try {
// Shopify's /cart/add.js endpoint
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: variantId, quantity }),
});
// Fetch full cart to get updated totals
const cartResponse = await fetch('/cart.js');
const cart = await cartResponse.json();
// Update state and open drawer to show the added item
set({ cart, isOpen: true });
} finally {
set({ isLoading: false }); // Hide loading state
}
},
// Toggle uses callback form to access current state
toggleDrawer: () => set((state) => ({ isOpen: !state.isOpen })),
// ... other actions
}));

Why Zustand:

  • Tiny bundle size (~1KB)
  • No boilerplate
  • Works outside React components
  • Built-in TypeScript support
  • Persist middleware for localStorage

Option 2: Jotai (Atomic State)

Jotai takes an atomic approach—each piece of state is an independent atom:

src/stores/atoms.ts
import { atom } from 'jotai';
import type { ShopifyCart } from '../types/shopify';
export const cartAtom = atom<ShopifyCart | null>(null);
export const cartOpenAtom = atom(false);
export const cartLoadingAtom = atom(false);
// Derived atom
export const cartItemCountAtom = atom((get) => {
const cart = get(cartAtom);
return cart?.item_count ?? 0;
});

Why Jotai:

  • Even smaller than Zustand
  • Fine-grained reactivity
  • Great for derived state
  • Suspense support

Option 3: React Context (Built-in)

For simpler themes, Context might be enough:

src/context/CartContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
const CartContext = createContext<CartContextType | null>(null);
export function CartProvider({ children }: { children: ReactNode }) {
const [cart, setCart] = useState(null);
const [isOpen, setIsOpen] = useState(false);
return (
<CartContext.Provider value={{ cart, setCart, isOpen, setIsOpen }}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) throw new Error('useCart must be used within CartProvider');
return context;
}

Why Context:

  • No additional dependencies
  • Familiar to React developers
  • Good for simple, contained state

Comparison Table

FeatureZustandJotaiContext
Bundle Size~1KB~2KB0KB
Learning CurveLowLowNone
BoilerplateMinimalMinimalModerate
DevToolsYesYesReact DevTools
Outside ReactYesLimitedNo
Best ForGlobal storesAtomic stateSimple apps

This Course: We’ll use Zustand for its simplicity and flexibility.

Additional Libraries

Here are other libraries we’ll use throughout the course:

UI & Animation

Terminal window
# Framer Motion - Animations
npm install framer-motion
# clsx - Conditional class names
npm install clsx

Data Fetching (Optional)

Terminal window
# TanStack Query - Server state management
npm install @tanstack/react-query

Forms

Terminal window
# React Hook Form - Form handling
npm install react-hook-form

Our Final Stack

Based on the analysis above, here’s the stack we’ll use:

CategoryChoiceRationale
Build ToolViteFast DX, simple config
LanguageTypeScriptType safety for Shopify objects
State ManagementZustandTiny, flexible, easy to learn
AnimationsFramer MotionProduction-ready animations
StylingCSS Modules + CSS VariablesWorks well with Liquid

Package.json Overview

Here’s what our package.json will look like:

{
"name": "shopify-react-theme",
"private": true,
"type": "module",
"scripts": {
"dev": "vite build --watch",
"build": "tsc && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"zustand": "^4.4.0",
"framer-motion": "^10.16.0",
"clsx": "^2.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}

Key Takeaways

  1. Vite is the modern choice for fast development and simple configuration
  2. TypeScript is strongly recommended for type-safe Shopify data handling
  3. Zustand provides lightweight global state with minimal boilerplate
  4. Keep dependencies minimal—every KB matters for e-commerce performance
  5. The stack should match your team’s skills and project requirements

In the next module, we’ll set up this development environment from scratch and configure Vite to output assets for your Shopify theme.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...