This commit is contained in:
Giancarmine Salucci
2026-02-18 01:21:44 +01:00
parent 54321fd7c9
commit 49bccf8f15
84 changed files with 14474 additions and 13925 deletions

View File

@@ -3,6 +3,7 @@
This guide documents SSR-safe patterns and anti-patterns for InstaRecipe development. All developers should follow these guidelines to prevent SSR errors.
## Table of Contents
- [Core Principle](#core-principle)
- [Browser API Detection](#browser-api-detection)
- [Lifecycle Hooks](#lifecycle-hooks)
@@ -18,6 +19,7 @@ This guide documents SSR-safe patterns and anti-patterns for InstaRecipe develop
**SvelteKit renders components on both the server and client.** Any browser-only APIs must be guarded or used in browser-only contexts.
### Browser-Only APIs (Require Guards)
- `window.*`
- `document.*`
- `localStorage`, `sessionStorage`
@@ -36,8 +38,8 @@ This guide documents SSR-safe patterns and anti-patterns for InstaRecipe develop
import { browser } from '$app/environment';
if (browser) {
// Safe: only runs in browser
const data = localStorage.getItem('key');
// Safe: only runs in browser
const data = localStorage.getItem('key');
}
```
@@ -49,14 +51,14 @@ if (browser) {
<script lang="ts">
import { browser } from '$app/environment';
import { onMount } from 'svelte';
let eventSource = $state<EventSource | null>(null);
function startSSEConnection() {
if (!browser) return; // ✅ Guard
eventSource = new EventSource('/api/stream');
}
onMount(() => {
if (browser) { // ✅ Explicit guard
startSSEConnection();
@@ -72,6 +74,7 @@ if (browser) {
### `onMount` - Browser-Only Lifecycle
**Use `onMount` for:**
- Browser API initialization
- Timer setup (`setInterval`, `setTimeout`)
- Event listener registration
@@ -81,12 +84,12 @@ if (browser) {
import { onMount } from 'svelte';
onMount(() => {
// ✅ Only runs in browser (built-in SSR guard)
const interval = setInterval(() => {
// Polling logic
}, 1000);
return () => clearInterval(interval); // Cleanup
// ✅ Only runs in browser (built-in SSR guard)
const interval = setInterval(() => {
// Polling logic
}, 1000);
return () => clearInterval(interval); // Cleanup
});
```
@@ -96,8 +99,8 @@ onMount(() => {
import { onDestroy } from 'svelte';
onDestroy(() => {
// ✅ Safe for cleanup
eventSource?.close();
// ✅ Safe for cleanup
eventSource?.close();
});
```
@@ -117,7 +120,7 @@ let stored = $state(localStorage.getItem('key')); // SSR crash!
// ✅ DO: Load in onMount
let stored = $state<string | null>(null);
onMount(() => {
stored = localStorage.getItem('key');
stored = localStorage.getItem('key');
});
```
@@ -142,29 +145,31 @@ let userAgent = $derived(navigator.userAgent); // SSR crash!
```typescript
// ❌ BAD: No browser guard
$effect(() => {
setInterval(() => checkHealth(), 1000); // SSR crash!
setInterval(() => checkHealth(), 1000); // SSR crash!
});
// ✅ GOOD: With browser guard
$effect(() => {
if (!browser) return;
const interval = setInterval(() => checkHealth(), 1000);
return () => clearInterval(interval);
if (!browser) return;
const interval = setInterval(() => checkHealth(), 1000);
return () => clearInterval(interval);
});
// ✅ BETTER: Use onMount for initialization instead
onMount(() => {
const interval = setInterval(() => checkHealth(), 1000);
return () => clearInterval(interval);
const interval = setInterval(() => checkHealth(), 1000);
return () => clearInterval(interval);
});
```
**When to use `$effect`:**
- Synchronizing derived state
- DOM manipulation (with browser guard)
- Reactive cleanup
**When NOT to use `$effect`:**
- Initialization (use `onMount`)
- API calls on mount (use `onMount`)
- Timer setup (use `onMount`)
@@ -189,11 +194,13 @@ if (browser && eventSource?.readyState === EventSource.OPEN)
```
**EventSource States:**
- `EventSource.CONNECTING = 0`
- `EventSource.OPEN = 1`
- `EventSource.CLOSED = 2`
**WebSocket States:**
- `WebSocket.CONNECTING = 0`
- `WebSocket.OPEN = 1`
- `WebSocket.CLOSING = 2`
@@ -220,8 +227,8 @@ const interval = setInterval(() => {}, 1000); // SSR crash!
// ✅ GOOD: In onMount
onMount(() => {
const interval = setInterval(() => {}, 1000);
return () => clearInterval(interval);
const interval = setInterval(() => {}, 1000);
return () => clearInterval(interval);
});
```
@@ -260,22 +267,23 @@ onMount(() => {
import { browser } from '$app/environment';
export class PushNotificationManager {
private static instance: PushNotificationManager | null = null;
static getInstance() {
if (!browser) return null; // ✅ Early return for SSR
// ... rest of implementation
}
private loadStoredSubscription() {
if (!browser) return null; // ✅ Guard localStorage
const stored = localStorage.getItem('pushSubscription');
return stored ? JSON.parse(stored) : null;
}
private static instance: PushNotificationManager | null = null;
static getInstance() {
if (!browser) return null; // ✅ Early return for SSR
// ... rest of implementation
}
private loadStoredSubscription() {
if (!browser) return null; // ✅ Guard localStorage
const stored = localStorage.getItem('pushSubscription');
return stored ? JSON.parse(stored) : null;
}
}
```
**Why it's good:**
- Guards all browser API access
- Early returns prevent unnecessary code execution during SSR
- Defensive programming with null checks
@@ -288,16 +296,16 @@ export class PushNotificationManager {
<script lang="ts">
import { browser } from '$app/environment';
import { onMount } from 'svelte';
let eventSource = $state<EventSource | null>(null);
onMount(async () => {
await loadQueueItems();
if (browser) { // ✅ Guard
startSSEConnection();
}
});
function startSSEConnection() {
if (!browser) return; // ✅ Double guard for safety
eventSource = new EventSource('/api/queue/stream');
@@ -316,7 +324,7 @@ export class PushNotificationManager {
```svelte
<script lang="ts">
import { onMount } from 'svelte';
onMount(() => {
// ✅ onMount only runs in browser
checkHealth(); // Initial check
@@ -327,6 +335,7 @@ export class PushNotificationManager {
```
**Why it's good:**
- Uses `onMount` instead of `$effect` for initialization
- Timer setup in browser-only context
- Proper cleanup with return function
@@ -344,7 +353,7 @@ let theme = $derived(localStorage.getItem('theme'));
// ✅ DO
let theme = $state<string | null>(null);
onMount(() => {
theme = localStorage.getItem('theme');
theme = localStorage.getItem('theme');
});
```
@@ -353,19 +362,19 @@ onMount(() => {
```typescript
// ❌ DON'T
$effect(() => {
// Runs during SSR!
fetch('/api/data');
// Runs during SSR!
fetch('/api/data');
});
// ✅ DO: Guard browser-specific side effects
$effect(() => {
if (!browser) return;
fetch('/api/data');
if (!browser) return;
fetch('/api/data');
});
// ✅ BETTER: Use onMount for initialization
onMount(() => {
fetch('/api/data');
fetch('/api/data');
});
```
@@ -387,8 +396,8 @@ const interval = setInterval(() => {}, 1000);
// ✅ DO
onMount(() => {
const interval = setInterval(() => {}, 1000);
return () => clearInterval(interval);
const interval = setInterval(() => {}, 1000);
return () => clearInterval(interval);
});
```
@@ -408,6 +417,7 @@ Watch server console for errors during build and preview.
### 2. Check for Hydration Warnings
Open browser DevTools console and look for:
- "Hydration failed"
- "The server response doesn't match the client content"
@@ -422,6 +432,7 @@ grep -r "navigator\." src/routes --include="*.svelte"
```
Then verify each usage is either:
- In an event handler (safe)
- In `onMount` (safe)
- Guarded with `if (browser)` (safe)