Browse learning paths

Taskboard 4. Async data

Use Request to load task data and Suspense to render pending, successful and rejected output for the asynchronous Tasks screen.

Table of contents

Stage 4. Load asynchronous task data

Starting state
Complete Stage 3 so the / route renders Taskboard with the shared store.
Applied change
Move the / route to TasksScreen, load /tasks.json once with request.get and pass the parsed tasks to store.replaceTasks.
Observable result
The route first shows Loading tasks and then the loaded Taskboard list. A rejected Request renders the Suspense error output.
Next step
Continue with Stage 5 and add a validated form to the loaded Tasks screen.

Observe Request through Suspense

Open Taskboard to see "Loading tasks..." followed by the parsed list. To observe rejection, change the Request URL to "/missing-tasks.json", rebuild and reload. Suspense renders its error branch for that non-OK Request.

Request result
request.get sets Accept: application/json by default, parses the response according to that header and rejects non-OK responses with an error that contains response and, when parsed, body.
Suspense pending
Suspense renders fallback while LoadedTasks and its Request are pending.
Suspense resolved or rejected
The resolved child replaces fallback. A rejection renders the error prop. suspenseKey identifies this host scope, so later renders with the same key reuse its resolved value or error.

public/tasks.json

json
[
  { "id": 1, "title": "Learn Valyrian.js" },
  { "id": 2, "title": "Build Taskboard" }
]

src/router.tsx

tsx
import { Router } from "valyrian.js/router";
import type { TaskboardStore } from "./state";
import { TasksScreen } from "./tasks-screen";

export function createRouter(store: TaskboardStore) {
  const router = new Router();
  router.add("/", () => (
    <>
      <button onclick={() => router.go("/about")}>About</button>
      <TasksScreen store={store} />
    </>
  ));
  router.add("/about", () => (
    <main>
      <h1>About Taskboard</h1>
      <button onclick={() => router.go("/")}>Tasks</button>
    </main>
  ));
  router.catch(404, () => <h1>Taskboard page not found</h1>);
  return router;
}

src/tasks-screen.tsx

tsx
import { request } from "valyrian.js/request";
import { Suspense } from "valyrian.js/suspense";
import { TaskboardApp } from "./app";
import type { Task, TaskboardStore } from "./state";

async function LoadedTasks({ store }: { store: TaskboardStore }) {
  if (store.state.loaded === false) {
    const tasks = (await request.get("/tasks.json")) as Task[];
    store.replaceTasks(tasks);
  }
  return <TaskboardApp store={store} />;
}

export function TasksScreen({ store }: { store: TaskboardStore }) {
  if (store.state.loaded) {
    return <TaskboardApp store={store} />;
  }

  return (
    <Suspense
      suspenseKey="taskboard:tasks"
      fallback={<p>Loading tasks...</p>}
      error={(error) => <p>{String(error)}</p>}
    >
      <LoadedTasks store={store} />
    </Suspense>
  );
}