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:
# Development server starts in millisecondsnpm run dev
# Hot Module Replacement is instant# No waiting for full rebuildsVite Advantages:
| Feature | Benefit |
|---|---|
| Native ES modules | Instant server start, no bundling needed in dev |
| Hot Module Replacement | Sub-second updates when you save files |
| Rollup-based production builds | Optimized, tree-shaken output |
| First-class TypeScript support | No additional configuration needed |
| Simple configuration | Less boilerplate than Webpack |
Basic Vite Config for Shopify:
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:
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:
| Feature | Benefit |
|---|---|
| Mature ecosystem | Plugin for nearly anything |
| Battle-tested | Predictable behavior in edge cases |
| Dawn theme compatibility | Easy integration with existing Dawn builds |
| More control | Fine-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: Strongly Recommended
TypeScript adds static typing to JavaScript. For a React-Shopify theme, it’s particularly valuable:
Why TypeScript for Shopify Themes?
- Shopify Object Types: Define interfaces for products, variants, and cart items
- Catch Errors Early: Find bugs at build time, not in production
- Better IDE Support: Autocomplete and inline documentation
- Refactoring Confidence: Rename properties safely across your codebase
Defining Shopify Types
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
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
{ "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.
Option 1: Zustand (Recommended)
Zustand is a small, fast, and unopinionated state management library:
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 stateexport 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:
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 atomexport 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:
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
| Feature | Zustand | Jotai | Context |
|---|---|---|---|
| Bundle Size | ~1KB | ~2KB | 0KB |
| Learning Curve | Low | Low | None |
| Boilerplate | Minimal | Minimal | Moderate |
| DevTools | Yes | Yes | React DevTools |
| Outside React | Yes | Limited | No |
| Best For | Global stores | Atomic state | Simple 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
# Framer Motion - Animationsnpm install framer-motion
# clsx - Conditional class namesnpm install clsxData Fetching (Optional)
# TanStack Query - Server state managementnpm install @tanstack/react-queryForms
# React Hook Form - Form handlingnpm install react-hook-formOur Final Stack
Based on the analysis above, here’s the stack we’ll use:
| Category | Choice | Rationale |
|---|---|---|
| Build Tool | Vite | Fast DX, simple config |
| Language | TypeScript | Type safety for Shopify objects |
| State Management | Zustand | Tiny, flexible, easy to learn |
| Animations | Framer Motion | Production-ready animations |
| Styling | CSS Modules + CSS Variables | Works 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
- Vite is the modern choice for fast development and simple configuration
- TypeScript is strongly recommended for type-safe Shopify data handling
- Zustand provides lightweight global state with minimal boilerplate
- Keep dependencies minimal—every KB matters for e-commerce performance
- 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...