Skip to content

Unwanted execution of promises faster than the hydration process #2534

@dennev

Description

@dennev

Describe the bug

Under certain conditions, the fetcher promise of 'createResource' in SSR can cause an initialization error when the execution time is short.

In 1.9.9, in syncblocks, and in async function blocks, if you have a promise with a short execution time as a fetcher, hydration fails with a key mismatch.

I borrowed the example from another issue, which is somewhat related.

You must access data.loading before data().

For example:

async :

  const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));

  const [data] = createResource(refetchTrigger, async () => {
    await sleep(1);
    return { foo: 421 };
  });
  return (
    <Suspense>
      <Show when={!data.loading && data()}>
        <context.Provider value={createStore(data())}>
          <Main />
        </context.Provider>
      </Show>
    </Suspense>
  );

sync :

  const [data] = createResource(refetchTrigger, () => {
    return Promise.resolve({ foo: 499 });
  });
// ...same here

I guess that the problem is that the promise resolves before it reaches some stable stage of hydration.
I've documented the problematic situations below.

Your Example Website or App

https://codeberg.org/iacore/kanban/src/branch/solidstart-debug/src/app.tsx

Steps to Reproduce the Bug or Issue

  1. Specify a log point.
// packages/solid/src/server/rendering.ts
// At the end of createResource
// ...
      p = p
        .then(res => {
          console.log(`res`,res)  // logger
          read.loading = false;
          read.state = "ready";
          ctx.resources[id].data = res;
          p = null;
          notifySuspense(contexts);
          return res;
        })
        .catch(err => {
          read.loading = false;
          read.state = "errored";
          read.error = error = castError(err);
          p = null;
          notifySuspense(contexts);
          throw error;
        });
      if (ctx.serialize) ctx.serialize(id, p, options.deferStream);
      return p;
    }
    return handleResolvedValue(p, ctx, id);
  }
  if (options.ssrLoadFrom !== "initial") load();
  const ref = [read, { refetch: load, mutate: (v: T) => (value = v) }] as ResourceReturn<T>;
  if (p) resource.ref = ref;
  console.log('returning ref', id) // logger
  return ref;
}
  1. Reduce execution time.
const [data] = createResource(refetchTrigger, async () => { // or resolved Promise in sync block
    await sleep(1);  // short time
    return { foo: 421 };
  });

  return (
    <Suspense>
      <Show when={!data.loading && data()}>
        <context.Provider value={createStore(data())}>
          <Main />
        </context.Provider>
      </Show>
    </Suspense>
  );
  1. Run
// console.log
returning ref 00000000100000010
returning ref 0000000010000020000000
res { foo: 421 }
returning ref 1
res Hello title!
res Hello title!
returning ref 00000000100000010
No route matched for preloading js assets
returning ref 1
res Hello title!
res Hello title!
returning ref 00000000100000010
No route matched for preloading js assets
returning ref 1
res Hello title!
res Hello title!

When latency is greater than 300 ms:

  1. Increase the execution time
  const [data] = createResource(refetchTrigger, async () => {
    await sleep(300);   // Change Here
    return { foo: 421 };
  });
  1. Run
// console.log
returning ref 00000000100000010
res Hello title!
returning ref 0000000010000020000000
returning ref 1
res Hello title!
res { foo: 421 }

This is thought to be a problem when the function execution time, whether synchronous or asynchronous, is shorter than the completion time of any process in the hydration.

Expected behavior

It should run normally without any errors.

Platform

  • OS: [Windows]
  • Browser: [Chrome]
  • Version: [139.0.7258.68 (Official Build) (64-bit)]

Additional context

Shouldn't we call data() first? You can.
For example, let's reverse the order in which the values are called in the Show component in our example.

// ...
      <Show when={data() && !data.loading}> // let's reverse the order!
// ...

And the log (keeping the logging points from above) would look like this

returning ref 00000000100000010
returning ref 0000000010000020000000
res { foo: 421 }
returning ref 1
res Hello title!
res Hello title!

But there's a potential problem here. The problem is that when the fetcher of createResource is executed, it is before any other component that interacts with the client or interacts between the client and the server has completed its initialization.

If there is some latency, the (probably ideal) log would be:

returning ref 00000000100000010
returning ref 0000000010000020000000
returning ref 1
res Hello title!
res Hello title!
res { foo: 421 }

I've only been looking at this for a short time, so maybe this was already taken into account at design time, but in my experience this is an unexpected and unwanted situation. This is because it has the potential to lead to unexpected results, as in this case of approaching loading first. So I'm raising it as an issue.

Alternatively, if we were to adhere to this usage, we would need to document that the order of calling these functions should be respected instead.

However, this won't solve:

  1. Calling the entire data(), which is unknown how large and massive it might be, is a concern.
  2. People expect to be able to call isLoading, loading, etc, to determine availability by accessing them first.
  3. Since loading is only available after initialization is complete, the logic of loading at initialization and loading on refresh is more separated, which can increase complexity.

Thanks!

Related Issues: #2132, solidjs/solid-start#1941

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions