Skip to content

Solid Hack Suspense Learnings

Posted on:November 28, 2024

So as you probably know by now I participated in a Solid Hackathon. In doing this I built the biggest SolidStart app I’ve done to date. (go vote for Solid Issue Tracker Lite)

I was tempted to do a complete how-to of building it, but instead want to share two learnings I had. Both of these learnings revolve around Suspense.

How createMemo interacts with Suspense

My first learning was how createMemo works with Suspense. Let’s look at an example.

Which fallback will render in the below case?

export default function Home() {
  return (
    <Suspense fallback="Home Loading...">
      <Issues />
    </Suspense>
  );
}

function Issues() {
  const issues = createAsync(() => getIssues());

  return <Suspense fallback="Issues Loading...">{issues()}</Suspense>;
}

If you said the Issues Loading... one you’d be correct. This is because the Suspense is triggered where the read happens.

Now let’s introduce a createMemo and think about what fallback shows.

export default function Home() {
  return (
    <Suspense fallback="Home Loading...">
      <Issues />
    </Suspense>
  );
}

function Issues() {
  const issues = createAsync(() => getIssues());
  const filteredIssues = createMemo(() =>
    issues().filter(issue => issue.createdAt > "2024-01-01")
  );

  return <Suspense fallback="Issues Loading...">{filteredIssues()}</Suspense>;
}

My initial thought would be that nothing would have changed. That the filtered issues would trigger the Issues Loading... fallback but this is incorrect. The memo is eager and actually triggers the Suspense above where the createMemo is defined which is Home Loading....

I’ve been told this will probably change in Solid 2.0 but this was something I didn’t know until recently.

How to avoid showing fallback with Suspense

So fallbacks are great for initial loading states, but once you have content to show you don’t always want to fall back to loading. You would rather fade out the content and then swap it in when its ready. This is exactly what transitions allow you to do.

In my case I had a list of issues, and when you selected a date filter I didn’t want it to go back to loading every time.

Transitions

export default function Issues(props) {
  const [dateFilter, setDateFilter] = createSignal<string>();
  const issues = createAsync(() => getIssues(dateFilter));
  const [pending, startTransition] = useTransition();

  return (
    <Suspense fallback="Loading...">
      <DatePicker
        onValueChange={value => {
          startTransition(() => {
            setDateFilter(value);
          });
        }}
      />
      <div classList={{ "opacity-50": pending }}>{/* Show issues */}</div>
    </Suspense>
  );
}

What you have to do is wrap the state setter call in a startTransition that you get from useTransition. You can then use this pending state to grey out the div as in this example.

One thing to note here is that transitions are global. So in this case if I trigger a server action someplace else it will put the issues into a pending state because actions use transitions under the hood.

.latest

Another way to prevent showing the fallback is to use the .latest property on an async value. Instead of using a transition I could do the following:

export default function Issues(props) {
  const [dateFilter, setDateFilter] = createSignal<string>();
  const issues = createAsync(() => getIssues(dateFilter));

  return (
    <Suspense fallback="Loading...">
      <DatePicker
        onValueChange={value => {
          setDateFilter(value);
        }}
      />
      <div classList={{ "opacity-50": !issues.latest }}>
        {JSON.stringify(issues.latest)}
      </div>
    </Suspense>
  );
}

Another note here that in Solid 2.0 this will probably become latest(issues) instead of issues.latest.

I hope you learnt something in this quick blog post about Suspense.