svelte-in-astro-context-race-condition-issue Svelte Themes

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:

  1. FormInput initializes first and calls getContext() → receives undefined
  2. FormWrapper initializes second and calls setContext() → 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:

  1. Individual Components as Islands: When FormWrapper is used directly as an Astro island, each Svelte component becomes a separate hydration unit
  2. Hydration Order: Astro hydrates components in DOM order (children before parents) rather than logical dependency order
  3. 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.

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

  • bug
  • svelte
  • hydration
  • context
  • islands
  • priority: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.

Top categories

Loading Svelte Themes