sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.69k stars 1.93k forks source link

SvelteKit fatal JS error: stop is not a function #7508

Closed kevinrenskers closed 1 year ago

kevinrenskers commented 2 years ago

Describe the bug

I have a problem where if I reach a certain page from client side navigation everything works fine. But if I then reload that page, the dev server crashes with the following:

/node_modules/svelte/store/index.mjs:59
                stop();
                ^

TypeError: stop is not a function
    at eval (/node_modules/svelte/store/index.mjs:59:17)
    at $$subscribe_locations (/src/routes/sub/+page.svelte:15:34)
    at eval (/src/routes/sub/+page.svelte:38:3)

Reproduction

I have a repro project at https://github.com/kevinrenskers/sveltekit-store-crash.

  1. Open the homepage, click on the "Go to subpage" link
  2. The new page shows "1: Test"
  3. Reload the page
  4. Crash

System Info

System:
    OS: macOS 13.0
    CPU: (10) arm64 Apple M1 Max
    Memory: 462.00 MB / 32.00 GB
    Shell: 3.5.1 - /opt/homebrew/bin/fish
  Binaries:
    Node: 16.17.0 - ~/Library/pnpm/node
    npm: 8.15.0 - ~/Library/pnpm/npm
  Browsers:
    Firefox: 106.0.1
    Safari: 16.1
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.87
    @sveltejs/kit: next => 1.0.0-next.535
    svelte: ^3.44.0 => 3.52.0
    vite: ^3.1.0 => 3.2.2

Severity

"Blocking all usage of SvelteKit"

Well, I am already using SvelteKit in production so that is not completely true. But it is blocking me from pushing a brand new feature to the website which now 100% breaks when reloading the website.

Additional Information

To explain a bit what my sample code is doing: in my real-world app I have centralized writable stores for a whole bunch of content, which is fetched from an external API. The real fetchContent function does the API request, and then the logic is basically the same: first check if we already have the content stored, if not do the API request. Then, if we're in the browser, store the fetched content in our store, otherwise just return a new readable store.

That way the page always has a readable store of content to work with, and all the cashing happens behind the scene.

The sample code is simplified to return hardcoded content and all the code is within one page, whereas in the real application the store and the fetchContent are within the lib folder, but the error is the same.

dummdidumm commented 1 year ago

The problem is that you are running an asynchronous operation inside the server context. Svelte can't really detect that and breaks, because while doing server side rendering, everything runs in one go synchronously, and by the time fetchContent resolves the component context no longer exists.

Wrap your fetchContent.then(..) inside if (browser) { .. } and it works.

This isn't really an issue with SvelteKit, rather a gotcha with Svelte server rendering itself, therefore closing.

kevinrenskers commented 1 year ago

It's strange how moving the exact same code to +page.ts does work, async operation and all.

// +page.ts
import { get, writable, readable, type Writable } from "svelte/store";
import { browser } from "$app/environment";

import type { PageLoad } from "./$types";

export const load: PageLoad = async ({ parent }) => {
  const locationsStore: Writable<{ id: number; name: string }[]> = writable();

  if (browser && get(locationsStore)) {
    return { locations: locationsStore };
  }

  const fetchedValues = [{ id: 1, name: "Test" }];

  if (browser) {
    locationsStore.set(fetchedValues);
    return { locations: locationsStore };
  } else {
    return { locations: readable(fetchedValues) };
  }
};
// +page.svelte
<script lang="ts">
  import type { PageData } from "./$types";
  export let data: PageData;
  const locations = data.locations;
</script>

{#each $locations as location}
  {location.id}: {location.name}
{/each}

In my real-world app it's actually a reusable component that calls this fetchContent function, so it's not as easy to refactor. I guess I just have to pass in the fetched content, which the page has to give it, and the fetching has to be done in +page.ts. But since the component is shown conditionally that means I'd be overfetching in some scenarios.

Adding the if (browser) { .. } like you suggested works, but then you have a flash of content appearing because the SSR version doesn't have the content, which isn't great.

I really wish SvelteKit would make async operation in components and pages work reliably for SSR, just like it can do in +page.ts.