simplify
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user