Skip to content

Add low-level suspense primitive #1877

Open
@thetarnav

Description

@thetarnav

A low-level suspense primitive would be useful for libraries to pause effects in "offscreen" branches.

Currently Suspense is not only a component-only primitive, but it is tied to resources, and by extend to routing, SSR, transitions, etc. It also assumes needing a fallback branch, which is not always needed.
Because of that it cannot be freely used in libraries, without affecting the rest of the app as a side effect.

For example in transition libraries like motionone and solid-transition-group, when we use <Transition> with mode="out-in", we need to wait for the previous element finish his exit animation, before the newly rendered element can be added to the DOM. But solid doesn't know that the new element is only kept in memory, and not yet appeared on the page, so the updates queue will proceed normally, calling all onMount callbacks, where we expect to deal with elements connected to the DOM.
An issue with more details: solidjs-community/solid-transition-group#34

Also if we wish to keep some roots in memory—e.g. to avoid recreating the same elements when filtering a large array, or displaying search highlights, or implementing a root pool primitive—there is no way to simply prevent then from running some side effects.

If we tried to use <Suspense> to suspend those branches, any resource read under it will also trigger it, possibly breaking the intended behavior of the app—by not showing a fallback in an expected place, or causing a transition (transaction) to be exited sooner (<Transition> is commonly used for wrapping rendered routes).

Code

example createSuspense using Suspense:
https://playground.solidjs.com/anonymous/21cef751-8f37-4354-8a63-0aa475d54e64

function createSuspense<T>(when: Accessor<boolean>, fn: () => T): T {
  let value: T,
    resolve = noop;

  const [resource] = createResource(
    () => when() || resolve(),
    () => new Promise<void>((r) => (resolve = r)),
  );

  Suspense({
    // @ts-expect-error children don't have to return anything
    get children() {
      createMemo(resource);
      value = fn();
    },
  });

  return value!;
}

In @fabiospampinato's oby this is solved by having a low-level suspense primitive that simply takes a function to suspend and a boolean signal to inform if the branch should be suspended or not, and returns the value directly, similar to createRoot.
When the condition signal is true, all effects will be suspended, while resources ignore it and keep the lookup for Suspense they can trigger.

Code

Something like this could maybe be implemented currently as this: (although I'm not sure if resources won't trigger it anyway)

function suspense<T>(when: Accessor<boolean>, fn: () => T): T {
  const SuspenseContext = getSuspenseContext(),
    store = {
      effects: [] as Computation<unknown>[],
      resolved: false,
      inFallback: when
    };

  let result!: T;

  SuspenseContext.Provider({
    value: store,
    get children() {
      result = fn();

      createMemo(() => {
        if (!when()) {
          store.resolved = true;
          resumeEffects(store.effects);
        }

        return result;
      });

      return undefined;
    }
  }) as any;

  return result;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions