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>
<span>{task.deadline}</span>
<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.