DEV Community

ANKUSH CHOUDHARY JOHAL
ANKUSH CHOUDHARY JOHAL

Posted on • Originally published at johal.in

The Unexpected internals with Preact and SvelteKit: Lessons Learned

The Unexpected Internals with Preact and SvelteKit: Lessons Learned

When building a recent e-commerce dashboard, our team opted to pair SvelteKit’s robust full-stack framework capabilities with Preact’s lightweight component model for high-interactivity widgets. The goal was to leverage SvelteKit’s built-in SSR, routing, and API endpoints while keeping bundle size low for frequently updated UI elements using Preact. What followed was a series of unexpected internal conflicts and hard-won lessons about framework interoperability.

1. Hydration Mismatches: The Silent Breakage

The first major hurdle was hydration mismatches between SvelteKit’s server-rendered output and Preact’s client-side hydration. SvelteKit renders pages using Svelte’s SSR engine, which outputs static HTML. When we embedded Preact components in SvelteKit pages, we initially used SvelteKit’s standard SSR for the entire page, then mounted Preact components on the client. This led to mismatches: SvelteKit’s SSR would add Svelte-specific data attributes (e.g., data-svelte) to DOM nodes, which Preact’s hydrateRoot did not expect, triggering console warnings and broken event listeners.

Lesson learned: For Preact components that require SSR, render them exclusively via Preact’s SSR APIs on the server, and ensure the client-side Preact hydration receives the exact same HTML. Avoid mixing Svelte and Preact SSR for overlapping DOM trees. We solved this by creating a custom SvelteKit endpoint that rendered Preact components to static HTML, then injected that HTML into SvelteKit pages, with matching client-side hydration.

2. Build Pipeline Quirks: Vite Plugin Conflicts

SvelteKit relies on Vite, and Preact has its own first-party Vite preset (@preact/preset-vite). Initially, we added both the SvelteKit and Preact Vite plugins to our config, leading to undefined behavior: JSX files were sometimes processed by Svelte’s JSX handler (which expects Svelte-specific syntax) instead of Preact’s, resulting in build errors. We also encountered duplicate React-like dependencies, as Preact’s preset aliases react to preact/compat, but SvelteKit’s build pipeline sometimes resolved the actual React package if not configured correctly.

Lesson learned: Explicitly define plugin order in vite.config.js, and use Vite’s resolve.alias to force all React imports to Preact. We also added a Vite plugins array order where Preact’s preset ran before SvelteKit’s plugin, and configured the esbuild option to only process .jsx/.tsx files with Preact’s JSX transform. This eliminated build conflicts and reduced bundle size by 12% by removing duplicate dependencies.

3. State Management Interop: Bridging the Gap

SvelteKit uses Svelte’s reactive stores for state, while Preact relies on hooks or external libraries like Valtio. We initially tried to share a Svelte store with a Preact component by passing it as a prop, but Svelte’s reactive updates did not trigger re-renders in Preact. Svelte stores use a subscribe method that Preact’s hook system does not automatically observe.

Lesson learned: Avoid deep state sharing across frameworks. For simple cases, pass primitive values as props and use Preact’s useEffect to watch for changes. For complex state, use a framework-agnostic library like Zustand, which works natively with both Svelte and Preact. We migrated our shared state to Zustand, wrapping store subscriptions in custom Preact hooks and Svelte stores to maintain reactivity on both sides.

4. Routing Edge Cases: Conflicting History Handling

SvelteKit’s client-side routing uses its own history API wrapper to enable prefetching and smooth transitions. When we added a Preact component that used preact-router for internal navigation, the two routing systems conflicted: clicking a link inside the Preact component would trigger both Preact’s routing and SvelteKit’s, leading to duplicate page loads or broken back button behavior.

Lesson learned: Disable framework-specific routing in Preact components when using SvelteKit. Rely exclusively on SvelteKit’s goto function for navigation, passed to Preact components via context or props. We also added a global event listener to intercept anchor tag clicks inside Preact components, routing them through SvelteKit’s navigation system to preserve prefetching and transition behavior.

Conclusion

Pairing Preact and SvelteKit is viable for projects that need SvelteKit’s full-stack features with Preact’s lightweight components, but it requires careful attention to internals. Key takeaways: separate SSR responsibilities per framework, configure Vite plugins explicitly, minimize cross-framework state sharing, and unify routing under SvelteKit. For most projects, sticking to a single framework is simpler, but for performance-critical use cases, this combo can work with the right guardrails.

Top comments (0)