Skip to main content

Overview

Follow these best practices to create high-quality, performant, and maintainable Shopify extensions with Synapse.

Performance Optimization

Bundle Size

Keep extensions under 50KB
  • Remove unused imports
  • Avoid large dependencies
  • Use tree-shaking
  • Code splitting

Load Time

Load in under 500ms
  • Lazy load components
  • Minimize initial render
  • Cache API responses
  • Optimize images

Re-renders

Minimize unnecessary renders
  • Use React.memo
  • Optimize useEffect deps
  • Memoize calculations
  • Avoid inline functions

Function Speed

Functions under 5ms
  • Use Rust over JavaScript
  • Minimize loops
  • Cache results
  • Limit query surface

Code Quality

TypeScript Best Practices

  • Strict Types
  • Type Guards
  • Interface Definitions
Use strict TypeScript configuration:
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}
Benefits:
  • Catch errors early
  • Better IDE support
  • Self-documenting code
  • Easier refactoring

Component Structure

1

Single Responsibility

Each component does one thing:
// ❌ Bad: Component does too much
function DeliveryEstimate() {
  const address = useShippingAddress();
  const lines = useCartLines();
  const [estimate, setEstimate] = useState(null);
  
  useEffect(() => {
    // Complex calculation
    // API calls
    // Formatting
  }, [address, lines]);
  
  return (
    <View>
      {/* Complex rendering */}
    </View>
  );
}

// ✅ Good: Separated concerns
function DeliveryEstimate() {
  const estimate = useDeliveryEstimate();
  return <DeliveryDisplay estimate={estimate} />;
}

function useDeliveryEstimate() {
  // Custom hook handles logic
}

function DeliveryDisplay({ estimate }) {
  // Component handles rendering
}
2

Extract Custom Hooks

Reuse logic with custom hooks:
// hooks/useDeliveryEstimate.ts
export function useDeliveryEstimate() {
  const address = useShippingAddress();
  const [estimate, setEstimate] = useState<DeliveryEstimate | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    if (!address) return;
    
    setLoading(true);
    calculateDeliveryDate(address)
      .then(setEstimate)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [address]);
  
  return { estimate, loading, error };
}

// Usage
function Extension() {
  const { estimate, loading, error } = useDeliveryEstimate();
  
  if (loading) return <SkeletonText />;
  if (error) return <ErrorBanner message={error} />;
  if (!estimate) return null;
  
  return <DeliveryDisplay estimate={estimate} />;
}
3

Memoize Expensive Operations

Use useMemo for calculations:
function Extension() {
  const lines = useCartLines();
  
  // ❌ Bad: Recalculates every render
  const total = lines.reduce((sum, line) => 
    sum + parseFloat(line.cost.totalAmount.amount), 0
  );
  
  // ✅ Good: Only recalculates when lines change
  const total = useMemo(() => 
    lines.reduce((sum, line) => 
      sum + parseFloat(line.cost.totalAmount.amount), 0
    ),
    [lines]
  );
  
  return <Total amount={total} />;
}

Function Optimization

Rust vs JavaScript

  • When to Use Rust
  • When to Use JavaScript
  • Performance Tips
Use Rust for:
  • Production functions
  • Performance-critical logic
  • Complex calculations
  • Large-scale operations
Benefits:
  • 10-100x faster than JS
  • Compiled to WebAssembly
  • Type safety
  • No garbage collection
Example:
// Fast execution, < 1ms typical
fn calculate_discount(lines: &[CartLine]) -> Vec<Discount> {
    lines.iter()
        .filter_map(|line| {
            if line.quantity >= 10 {
                Some(create_discount(line, 20.0))
            } else {
                None
            }
        })
        .collect()
}

Error Handling

Handle failures without breaking:
function Extension() {
  const { data, error } = useQuery();
  
  // ❌ Bad: Throws error
  if (error) throw error;
  
  // ✅ Good: Shows fallback
  if (error) {
    return (
      <Banner status="warning">
        Unable to load delivery estimate. 
        Standard shipping applies.
      </Banner>
    );
  }
  
  // ✅ Good: Degrades gracefully
  if (!data) {
    return <SkeletonText />;  // Loading state
  }
  
  return <Content data={data} />;
}
Catch component errors:
import { ErrorBoundary } from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
  'purchase.checkout.block.render',
  () => (
    <ErrorBoundary
      onError={(error) => {
        console.error('Extension error:', error);
        // Log to monitoring service
      }}
    >
      <Extension />
    </ErrorBoundary>
  )
);
Never panic in functions:
// ❌ Bad: Panics on error
let quantity = line.quantity.parse::<i64>().unwrap();

// ✅ Good: Returns default
let quantity = line.quantity.parse::<i64>().unwrap_or(0);

// ✅ Good: Returns empty on error
fn run(input: Input) -> Result<FunctionRunResult> {
    let operations = match calculate_operations(&input) {
        Ok(ops) => ops,
        Err(e) => {
            eprintln!("Error: {}", e);
            vec![]  // Return empty, don't crash
        }
    };
    
    Ok(FunctionRunResult { operations })
}
Explain errors clearly:
// ❌ Bad: Technical jargon
<Banner status="critical">
  GraphQL query failed: Field 'deliveryAddress' not found
</Banner>

// ✅ Good: User-friendly
<Banner status="info">
  Unable to calculate delivery estimate. 
  Please enter your shipping address.
</Banner>

// ✅ Good: Actionable
<Banner status="warning">
  Delivery estimate temporarily unavailable.
  <Button onPress={retry}>Try Again</Button>
</Banner>

Testing

  • Unit Tests
  • Integration Tests
  • Function Tests
Test individual functions:
// utils.test.ts
import { describe, it, expect } from 'vitest';
import { calculateDeliveryDate } from './utils';

describe('calculateDeliveryDate', () => {
  it('adds business days correctly', () => {
    const result = calculateDeliveryDate({
      country: 'US',
      province: 'CA'
    });
    
    expect(result.startDate).toBeDefined();
    expect(result.endDate).toBeDefined();
    expect(result.confidence).toBe('high');
  });
  
  it('handles invalid addresses', () => {
    const result = calculateDeliveryDate({
      country: 'XX'
    });
    
    expect(result.error).toBe('Invalid country');
  });
  
  it('accounts for weekends', () => {
    const result = calculateDeliveryDate({
      country: 'US'
    }, new Date('2025-10-31')); // Friday
    
    // Should skip weekend
    expect(result.startDate).not.toBe('2025-11-01');
  });
});

Accessibility

Keyboard Navigation

Support keyboard users:
// ✅ Use Button component
<Button onPress={handleClick}>
  Submit
</Button>

// ❌ Don't use div
<div onClick={handleClick}>Submit</div>

Screen Readers

Label everything:
// ✅ Good labels
<TextField
  label="Gift message"
  value={message}
  onChange={setMessage}
/>

<Icon 
  source="delivery" 
  accessibilityLabel="Delivery truck"
/>

Color Contrast

Ensure readability:
  • Use Shopify UI components (compliant by default)
  • Avoid custom colors
  • Test with contrast checker
  • Minimum 4.5:1 ratio

Focus States

Visible focus indicators:
  • Shopify components have focus states
  • Don’t remove with CSS
  • Test with Tab key
  • Ensure focus order logical

Security

Validate all user inputs:
function validateGiftMessage(message: string): string | null {
  if (message.length > 200) {
    return "Message too long (max 200 characters)";
  }
  
  if (/<script|javascript:/i.test(message)) {
    return "Invalid characters detected";
  }
  
  if (!message.trim()) {
    return "Message cannot be empty";
  }
  
  return null; // Valid
}
Secure API calls:
// ✅ Good: HTTPS, authentication
const response = await fetch('https://api.example.com/endpoint', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${sessionToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ data }),
});

// ❌ Bad: HTTP, no auth
fetch('http://api.example.com/endpoint', {
  method: 'POST',
  body: data,
});
Never log or expose:
// ❌ Bad: Logs sensitive data
console.log('Customer data:', customer);
console.log('Payment info:', paymentMethod);

// ✅ Good: Logs safely
console.log('Processing order:', { orderId: order.id });
console.log('Payment method type:', paymentMethod.type);

Deployment Best Practices

1

Test Locally First

npm run dev
# Test in browser
# Verify all features work
2

Review MCP Validation

Check validation output before deploying
  • All APIs valid
  • No deprecated components
  • TypeScript compiles
  • No console errors
3

Monitor Deployment

Watch logs during deployment:
shopify app function logs --watch
Verify deployment succeeds
4

Test in Dev Store

  • Add items to cart
  • Go through checkout
  • Test all code paths
  • Verify on mobile
5

Monitor Performance

  • Check loading times
  • Review error rates
  • Monitor function execution time
  • Track user feedback

Documentation

Write clear documentation:
# Delivery Estimate Extension

Shows estimated delivery dates in checkout.

## Features

- Calculates delivery based on shipping address
- Displays date range (e.g., "Nov 5-8")
- Handles international shipping
- Shows loading state while calculating

## Configuration

Configure in Shopify admin:
1. Settings → Checkout → Customize
2. Add "Delivery Estimate" block
3. Position after shipping address
4. Save

## Technical Details

- Target: `purchase.checkout.delivery-address.render-after`
- Uses: `useShippingAddress()` hook
- API: No external calls, pure calculation
- Performance: < 100ms load time

## Troubleshooting

**Not showing?**
- Verify added to checkout editor
- Check shipping address is entered
- Review console for errors

Next Steps