tsdevau/svelte-in-astro-context-race-condition-issue
Minimal reproduction case demonstrating hydration timing race condition when using Svelte 5's context API within Astro islands
Hydration timing race condition with Svelte 5 context API in Astro islands
Summary
There's a race condition in Astro's hydration process when using Svelte 5 components with the context API. Child components calling getContext() are hydrated before parent components calling setContext(), causing context to be undefined and breaking component communication.
Environment
- Astro Version: ^5.11.2
- Svelte Version: ^5.36.5 with Runes
- Astro Svelte Integration: @astrojs/svelte ^7.1.0
- TypeScript: ^5.8.3
- Hydration Strategy:
client:load - Architecture: Static-first with Astro islands
Problem Description
When using Svelte 5's context API within Astro islands, child components consistently hydrate before their parent components, causing getContext() to return undefined because setContext() hasn't been called yet.
Expected Behavior
Parent components should hydrate first and call setContext() before child components hydrate and call getContext().
Actual Behavior
Child components hydrate first, call getContext(), receive undefined, and fail to establish proper parent-child communication.
Reproduction Steps
1. Create Parent Component (FormWrapper.svelte)
<script lang="ts">
import type { FormContext } from "@/types"
import { setContext } from "svelte"
interface Props {
children?: any
}
let { children }: Props = $props()
console.log("FormWrapper: Starting initialization")
const formContext: FormContext = $state({
submitMessage: "",
setSubmitMessage: (message: string) => {
formContext.submitMessage = message
},
})
console.log("FormWrapper: Setting context")
setContext("form", formContext)
console.log("FormWrapper: Context set complete")
</script>
<form>
{@render children?.()}
</form>2. Create Child Component (FormInput.svelte)
<script lang="ts">
import type { FormContext } from "@/types"
import { getContext } from "svelte"
import type { HTMLInputAttributes } from "svelte/elements"
interface Props extends HTMLInputAttributes {}
let { name, type = "text", placeholder }: Props = $props()
console.log("FormInput: Starting initialization")
const formContext = getContext<FormContext>("form")
console.log("FormInput: Context received:", $inspect(formContext))
if (!formContext) {
console.error("FormInput: No form context found!")
}
</script>
<input
{name}
{type}
{placeholder}
/>3. Use in Astro Page
---
// src/pages/index.astro
import FormInput from "@components/FormInput.svelte"
import FormWrapper from "@components/FormWrapper.svelte"
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<title>GitHub Issue Test - Astro Svelte Context Race Condition</title>
</head>
<body>
<FormWrapper client:load>
<FormInput
name="email"
type="email"
placeholder="Enter email"
/>
</FormWrapper>
</body>
</html>4. Observe Console Output
FormInput: Starting initialization
FormInput: Context received: undefined
FormInput: No form context found!
FormWrapper: Starting initialization
FormWrapper: Setting context
FormWrapper: Context set complete
Debug Evidence
The console logs clearly show the timing issue:
FormInputinitializes first and callsgetContext()→ receivesundefinedFormWrapperinitializes second and callssetContext()→ too late
Working Workaround
Creating an intermediary Svelte component that wraps both parent and child resolves the issue:
<!-- FormNewsletter.svelte -->
<script lang="ts">
import FormWrapper from "./FormWrapper.svelte"
import FormInput from "./FormInput.svelte"
</script>
<FormWrapper>
<FormInput
name="email"
type="email"
placeholder="Enter email"
/>
</FormWrapper><!-- In Astro page -->
<FormNewsletter client:load />This works because both components are now in the same Svelte compilation context and hydrate together.
Root Cause Analysis
The issue appears to be in Astro's hydration process where:
- Individual Components as Islands: When
FormWrapperis used directly as an Astro island, each Svelte component becomes a separate hydration unit - Hydration Order: Astro hydrates components in DOM order (children before parents) rather than logical dependency order
- Context Isolation: Each component hydrates independently without awareness of context dependencies
Potential Solutions
1. Hydration Dependency Analysis
Astro could analyze Svelte components for context dependencies and adjust hydration order accordingly.
2. Context API Bridge
Implement a bridge that allows context to be established across separate hydration boundaries.
3. Deferred Context Resolution
Allow getContext() to return a promise or reactive value that resolves when context becomes available.
4. Hydration Grouping
Provide a way to group related components so they hydrate together as a unit.
Related Issues
This appears to be a broader issue affecting multiple frameworks:
- SvelteKit: Similar context timing issues reported in issue #1171
- Flowbite-Svelte: Context hydration problems documented in issue #1200
- Pattern: Common across SSR/hydration scenarios with context APIs
Impact
- Severity: High - Breaks fundamental parent-child communication patterns
- Frequency: Consistent - Occurs every time context API is used across Astro islands
- Workaround: Available but requires architectural changes
Suggested Labels
bugsveltehydrationcontextislandspriority:high
Technical Details
- Astro Integration:
@astrojs/svelte - Hydration Strategy:
client:load(affects all strategies) - Component Architecture: Parent/child with context communication
- Reproduction Rate: 100% consistent
Additional Context
This issue specifically affects Astro's islands architecture where components are hydrated independently. The problem doesn't occur in traditional SPA scenarios where all components hydrate together in a single context.
The workaround of using intermediary components is functional but forces architectural compromises and reduces the flexibility of Astro's component model.