Core Concepts
This guide introduces the key concepts and patterns used in our frontend architecture.
Architecture Overview
Section titled “Architecture Overview”Our frontend is built with:
- Astro: Zero-JS-by-default page rendering
- SolidJS: Interactive component framework with SSR support
- TypeScript: Type safety throughout
- Tailwind: Utility-first styling
- GraphQL: Data fetching via
graphql-request
Key Patterns
Section titled “Key Patterns”1. Component Type Selection
Section titled “1. Component Type Selection”Choose between .astro and .tsx based on these criteria:
Use .astro when:
Section titled “Use .astro when:”- Component is purely static
- Component needs server-side data fetching
- Component is primarily markup with minimal logic
- Component uses complex template composition
- Component doesn’t need client-side state
Example of a good .astro component:
---// Good .astro use case: Static layout with data compositioninterface Props { title: string; items: Array<{ id: string; name: string }>;}
const { title, items } = Astro.props;const groupedItems = groupByCategory(items);---
<section class="py-8"> <h2 class="text-24 font-bold">{title}</h2> { Object.entries(groupedItems).map(([category, items]) => ( <div class="mt-4"> <h3 class="text-20">{category}</h3> <ul class="grid grid-cols-3 gap-4"> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> )) }</section>Use .tsx when:
Section titled “Use .tsx when:”- Component needs client-side interactivity
- Component maintains client-side state
- Component handles user events
- Component needs lifecycle hooks
- Component uses SolidJS features
Example of a good .tsx component:
// Good .tsx use case: Interactive feature with stateinterface Props { initialItems: string[];}
export const FilterableList: Component<Props> = (props) => { const [searchTerm, setSearchTerm] = createSignal(""); const [loading, setLoading] = createSignal(false);
const filteredItems = createMemo(() => props.initialItems.filter(item => item.toLowerCase().includes(searchTerm().toLowerCase()) ) );
return ( <div> <input type="search" value={searchTerm()} onInput={(e) => setSearchTerm(e.currentTarget.value)} /> <Show when={!loading()} fallback={<LoadingSpinner />} > <ul> {filteredItems().map(item => ( <li>{item}</li> ))} </ul> </Show> </div> );};2. Component Composition
Section titled “2. Component Composition”We use a mix of static and interactive components to build features:
---// Example of component compositionimport { PageLayout } from "#components/layout";import { StaticHeader } from "#components/header";import { InteractiveFeature } from "#components/feature";import { StaticFooter } from "#components/footer";---
<PageLayout> <StaticHeader />
<!-- Critical interactive feature --> <InteractiveFeature client:load />
<!-- Below-fold interactive features --> <InteractiveFeature client:visible />
<!-- Non-critical features --> <InteractiveFeature client:idle />
<StaticFooter /></PageLayout>Key composition patterns:
- Use static components for layout and structure
- Add interactive components strategically
- Choose appropriate client directives
- Keep components focused and composable
3. Component Organization
Section titled “3. Component Organization”Components are organized by feature and type:
src/├── components/ # Shared components│ └── feature/│ ├── index.astro # Main component│ ├── Component.tsx # Interactive parts│ └── utils.ts # Helper functions├── pages/ # Routes and pages│ └── route/│ ├── _components/ # Route-specific (`_` prefixed to hide automatic routing)│ └── index.astro # Page template4. State Management
Section titled “4. State Management”We follow these patterns for state:
-
Component State: Use SolidJS signals
const [count, setCount] = createSignal(0); -
Route State: Use URL parameters
const params = new URLSearchParams(location.search); -
Shared State: Use nanostores
stores/cartStore.ts export const cartStore = map({items: [] as CartItem[],});
5. Error Handling
Section titled “5. Error Handling”We use structured error handling with Airbrake:
try { await operation();} catch (err) { const { airbrakeClient } = await import("#utils/airbrake/browser"); airbrakeClient.notify({ error: err, context: { component: "ComponentName" }, });}6. Image Optimization
Section titled “6. Image Optimization”We use Imgix for image optimization:
<ImgixImage src={src} width={800} height={600} loading="lazy" imgixParams={{ auto: "format,compress", q: 75 }}/>Development Workflow
Section titled “Development Workflow”-
Component Development
- Start with static Astro component
- Add interactivity only when needed
- Use appropriate client directive
- Implement error handling
- Add loading states
-
State Management
- Keep state close to where it’s used
- Use URL for route-level state
- Use stores for truly shared state
-
Performance
- Default to static components
- Optimize images with Imgix
- Use appropriate loading strategies
- Monitor with Grafana Faro
-
Error Handling
- Use structured error reporting
- Implement fallback UI
- Log errors with context
Anti-Patterns to Avoid
Section titled “Anti-Patterns to Avoid”❌ Don’t: Add client-side JavaScript by default ✅ Do: Start with static components, add interactivity only when needed
❌ Don’t: Use global state for component-level concerns ✅ Do: Keep state as close as possible to where it’s used
❌ Don’t: Skip error handling or use console.log ✅ Do: Use structured error reporting with Airbrake
❌ Don’t: Use direct DOM manipulation ✅ Do: Use framework-provided state management
Next Steps
Section titled “Next Steps”- Review the Component Patterns guide
- Understand our State Management approach
- Learn about Error Handling