Learning Remix: Pending UI

We can add and delete tasks, but the UI isn’t showing us what is happening if we have a slow network. We should tell the user that we’re adding or deleting. And we should do it while they wait for the database communication to happen.

Remix has a hook called useTransition that provides a transition object. It comes with some properties. One such property is transition.state. It has three values: “idle”, “submitting” and “loading”. We can use this to disable the button that the user clicked, and also change its appearance.

One problem though. This is a global, there is only one transition object. We need more information to identify which button the user clicked and only change the right one.

It turns out that the transition object has other properties that we can use. The one we want is transition.submission, that contains our formData.

We can query transition.submission.formData.get("_action") and check its value. If it is “create”, we know that the user clicked the “Add…” button. If it is “delete” it’s one of the “delete” buttons. But which one?

We’ll use a different method to figure that out. Let’s concentrate on the adding now.

First, add useTransition to the imports from remix:

import { Form, useLoaderData, useTransition } from "remix";

Then add this to your Index(), at the top:

let transition = useTransition();
let isAdding = transition.state === "submitting" &&
 transition.submission.formData.get("_action")==="create";

This sets isAdding to true while our app is busy receiving data and talking to the database. Then it reverts back to false.

Now, let’s use the value of isAdding to give our users feedback. We’ll change the text in the button, and add a disabled state while we’re waiting for everything to finish up:

<button type="submit" name="_action" value="create" disabled={isAdding}>
{isAdding ? "Adding your task": "Add Task"}
</button>

Open the developer tools in your browser and throttle the network to “Slow 3G”. Now the text changes and the button becomes disabled when you click it. Until the page reloads and the new task shows up in the list. Then the button goes back to normal again.

We can do something similar when we remove tasks, but we can’t use the exact same method. As mentioned above, transition is a global Singleton. Here it’s easier to use another of Remix’ hooks: useFetcher.

This is a hook that allows you to create forms that don’t trigger a navigation, but still make all routes reload. It is useful in many situations, a quote from the documentation:

This is useful when you need to:

  • fetch data not associated with UI routes (popovers, dynamic forms, etc.)
  • submit data to actions without navigating (shared components like a newsletter sign ups)
  • handle multiple concurrent submissions in a list (typical “todo app” list where you can click multiple buttons and all be pending at the same time)
  • infinite scroll containers
  • and more!

Notice they even mention “todo app”, so we’re on the right track!

We’ll use it inside the TodoTask.

We need to do three things: add an import of useFetcher from remix at the top. Create a binding to the hook, and change the Form into a fetcher.Form.

This means our TodoTask now looks like this:

import { Form, useFetcher } from "remix";
import { ITask } from "../utilities/interfaces";
import React from "react";

interface Props {
  task: ITask;
}

const TodoTask = ({ task }: Props) => {
  let fetcher = useFetcher();
  return (
    <div key={task.id}>
      <div>
      <fetcher.Form method="post">
          <input type="hidden" name="ID" value={task.id}></input>
        <span>{task.task}</span>&nbsp;
        <span>{task.deadline}</span>&nbsp;
      <button
        type="submit" name="_action" value="delete"
      >×</button></fetcher.Form>
      </div>
    </div>
  );
};

export default TodoTask;

Now we can start fixing the pending state for deletions. We check that we have an ongoing submission. We do it by binding fetcher.submission.formData.get("ID") to isDeleting.

We need to be careful though. Most of the time we don’t have a submission so fetcher.submission.formData will cause an error.

To avoid that we use the “optional chaining operator”:

let isDeleting = fetcher.submission?.formData.get("ID");

You can add that line below the call to useFetcher().

Now we can show the user something happens when they click the ”×” for that particular task. And it won’t affect any of the other tasks. This would’ve been a lot more difficult if we hadn’t extracted the task into its own component.

Let’s use it to style the component, making it transparent while deletion is in progress. Add a style attribut to the main div that wraps our whole component, like this:

<div key={task.id} className="task"
style={{opacity: isDeleting ? 0.25 : 1}} >

This will make the task 25% transparent, while deletion is in progress. And opaque otherwise. Perfect. Or?

We’ll look at another approach in a later instalment in the series: optimistic UI.

With optimistic UI we remove the task immediately, before we delete it from the database. We will also add tasks to the UI before we add them to the database.

This comes with a few problems. What happens if we can’t add to or delete from the database? We’ll show you.

But first we’ll fix another problem with the form we use to add new tasks. If a user wants to type new tasks in rapid succession, it’s not easy as it is now. The inputs aren’t cleared after each submission. Let’s address that in the next part of this series.

About the author

For the last three decades, I've worked with a variety of technologies. I'm currently focused on fullstack development. On my day to day job, I'm a senior teacher and course developer at a higher vocational school in Malmoe, Sweden. I'm also an occasional tech speaker and a mentor. Do you want to know more? Visit my website!