Skip to content

[2.0 / next] <Errored> stays on its fallback after navigating away when a <Show>/<Match> condition read the failed fetch #4644

Closed

Description

@brenelz

Describe the bug

A list page reads an async memo, and the list fetch fails, so the <Errored> around the page shows its fallback. The user then navigates to another page, which is a different branch under the same <Errored>. If the list page read the failed memo in a <Show when> or a <Match when>, for example the usual empty-state check <Show when={rows().length === 0}>, the boundary keeps showing its fallback after the navigation, and the app stays on the error until a reload.

A plain read of the same memo (text, <For>, <Repeat>, an attribute, latest()) recovers on navigation. A wider sweep found the same failure for a memo of the condition used in a <Show>, for a keyed <Show>, and with or without a <Loading> around the page, and with the navigation done by <Show> or by <Switch>. Recovery by refresh() or by changing the memo's input works in every case.

Found by fuzzing reactivescript's example apps (a notes app whose list uses an empty-state <Show>), then reduced to plain Solid.

Your Example Website or App

The vitest spec below (in packages/web/test/), on next @ 4235019.

Steps to Reproduce the Bug or Issue

import { render } from "@solidjs/web";
import { createMemo, createSignal, Errored, flush, For, Loading, Match, Show, Switch } from "solid-js";
import { expect, it } from "vitest";

const tick = async () => { for (let i = 0; i < 2; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
type Row = { id: number };

const readers: Record<string, (rows: () => Row[]) => any> = {
  "Show when={rows().length === 0}": rows => <Show when={rows().length === 0} fallback={<p>{rows().length} rows</p>}><p>no rows</p></Show>,
  "Show when={rows()}": rows => <Show when={rows()}>{r => <p>{r().length} rows</p>}</Show>,
  "Switch/Match": rows => <Switch fallback={<p>{rows().length} rows</p>}><Match when={rows().length === 0}><p>no rows</p></Match></Switch>,
  "text (control)": rows => <p>{rows().length} rows</p>,
  "For (control)": rows => <ul><For each={rows()}>{r => <li>{r.id}</li>}</For></ul>,
};

for (const [name, view] of Object.entries(readers))
it(`${name}: navigating away from a failed list clears <Errored>`, async () => {
  let rejectList!: (e: Error) => void;
  const el = document.createElement("div");
  let setPage!: (p: string) => void;
  const dispose = render(() => {
    const [page, set] = createSignal("list"); setPage = set;
    const rows = createMemo(() => new Promise<Row[]>((_, reject) => { rejectList = reject; }));
    return (
      <Errored fallback={<p>could not load</p>}>
        <Show when={page() === "list"} fallback={<p>about page</p>}>
          <Loading fallback={<p>loading</p>}>{view(rows)}</Loading>
        </Show>
      </Errored>
    );
  }, el);
  await tick();
  rejectList(new Error("boom")); await tick();
  const failed = el.textContent;
  setPage("about"); flush(); await tick();
  const after = el.textContent;
  dispose();
  expect({ failed, after }).toEqual({ failed: "could not load", after: "about page" });
});

npx vitest run in packages/web. The two control tests pass; the Show and Switch/Match tests fail with after: "could not load".

Expected behavior

After navigating away, nothing under the boundary reads the failed memo, so <Errored> renders the new branch (about page), as it does when the list page reads the memo as text or in a <For>.

Platform

  • OS: macOS
  • Browser: n/a (jsdom)
  • Version: solid-js 2.0.0-rc.20, next @ 4235019

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