Skip to content

Optimistic UI With SolidStart

Posted on:April 13, 2024

Today we are going to learn about how to create an optimistic ui using SolidStart.

Optimistic ui essentially means showing the happy path state quickly without needing a network request to finish. Then in the unlikely event that something goes wrong we rollback and undo that change.

I’ve chosen to show this by creating a simple array of objects in memory that we can push to. No database needed!

An artificial delay has also been added so you can see the problem with our ui if the network is slow.

We will gloss over actions and createAsync for this tutorial. If you want to learn more about them check the docs.

Project Setup

Firstly, create a new project using pnpm create solid@latest and choose the basic template. This gives us access to server actions.

Then run the project with pnpm dev and open up localhost:3000 in the browser.

We will do all our coding inside the routes/index.jsx and keep things as simple as possible to focus on the optimistic ui.

Modify your index.jsx to the following boilerplate that showcases the problem.

import {
  action,
  cache,
  createAsync,
  useAction,
  useSubmissions,
} from "@solidjs/router";
import { For } from "solid-js";

const todos = [
  {
    title: "Todo 1",
  },
  {
    title: "Todo 2",
  },
];

const getServerTodos = cache(async () => {
  "use server";
  return todos;
}, "get-server-todos");

const addTodo = action(async title => {
  "use server";
  await new Promise(r => setTimeout(r, 2000));

  todos.push({
    title,
  });
}, "add-todo");

export default function Home() {
  const serverTodos = createAsync(() => getServerTodos());
  const addTodoAction = useAction(addTodo);

  return (
    <main>
      <h1>Todos</h1>
      <button onClick={() => addTodoAction("Todo 3")}>Add Todo</button>
      <For each={serverTodos()}>{todo => <h3>{todo.title}</h3>}</For>
    </main>
  );
}

The Problem

Take a look at the following video to see the problem. The adding of todos has a delay that makes the ui feel sluggish.

The Fix

Let’s fix our problem. SolidStart gives us a useSubmissions hook which allows us to get access to pending submissions before they get sent to the server.

import { useSubmissions } from "@solidjs/router";

export default function Home() {
  const todoSubmissions = useSubmissions(addTodo);
}

Before we start using our submissions let’s stub out a new todos variable that will contain both our serverTodos as well as our pendingTodos.

import {
  action,
  cache,
  createAsync,
  useAction,
  useSubmissions,
} from "@solidjs/router";
import { For } from "solid-js";

export default function Home() {
  const serverTodos = createAsync(() => getServerTodos());
  const addTodoAction = useAction(addTodo);

  const todoSubmissions = useSubmissions(addTodo);
  const pendingTodos = () => [];

  const todos = () =>
    serverTodos() ? [...serverTodos(), ...pendingTodos()] : [];

  return (
    <main>
      <h1>Todos</h1>
      <button onClick={() => addTodoAction("Todo 3")}>Add Todo</button>
      <For each={todos()}>{todo => <h3>{todo.title}</h3>}</For>
    </main>
  );
}

Wrapping these variables in functions might look a bit odd if coming from React. This allows these functions to re-run if the signals they reference change.

Now in our case this hasn’t really changed anything because our pendingTodos are always empty. Let’s replace that with proper optimistic ui logic!

const pendingTodos = () =>
  [...todoSubmissions.values()]
    .filter(todoSubmission => todoSubmission.pending)
    .map(todoSubmission => ({
      title: todoSubmission.input[0] + " (pending)",
    }));

Let’s break this logic down as you can’t just map over the submissions directly. You have to call the values function and spread it into a new array.

const pendingTodos = () => [...todoSubmissions.values()];

We also only want to add the pending ones, otherwise our todo will show up twice once its finalized on the server.

const pendingTodos = () =>
  [...todoSubmissions.values()].filter(
    todoSubmission => todoSubmission.pending
  );

Lastly we want the pending item to look similar to our serverTodos which are objects with a title property. SolidStart passes us the input to our action so we can use that to recreate the object.

.map(todoSubmission => ({
    title: todoSubmission.input[0] + " (pending)",
}));

Now look how snappy our ui feels even with the two second delay. The item shows instantly, and then when it’s finalized in the db the (pending) text goes away.