Optimistic UI with Remix

In this documentation, we will explore how Remix can help developers implement an Optimistic UI in their applications. Optimistic UI is a technique where we update the user interface instantly to reflect user actions, without waiting for a server response. This approach provides a better user experience by giving immediate feedback and reducing perceived latency.

Primary Use Case for Optimistic UI

The primary use case for Optimistic UI is to enhance the user experience by displaying information to the user as soon as it is updated, without waiting for a server response. For example, in a social media application, when a user likes a post, the like count should be updated immediately, without waiting for a server response to confirm the action.

Using the Native Form Component from @remix-run/react Library

We can achieve Optimistic UI using the native Form component from the @remix-run/react library. Let's see an example:

import { Form, useLoaderData } from "@remix-run/react";
import type { ActionFunction, LoaderFunction } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { useState } from "react";

// LoaderFunction
export const loader: LoaderFunction = async () => {
  // Fetch data here
  return json({ likeCount: 0, postId: 1 });
};

// ActionFunction
export const action: ActionFunction = async ({ request }) => {
  // Perform action here, e.g., updating a like count

  return json({ status: "success" });
};

export const LikeButton = () => {
  const data = useLoaderData();
  const [likeCount, setLikeCount] = useState(data?.likeCount);
  
  const handleSubmit = () => {
    // Update the UI optimistically
    setLikeCount(likeCount + 1);
  };

  return (
    <Form method="post" onSubmit={handleSubmit}>
      <input type="hidden" name="postId" value={data?.postId} />
      <button type="submit">Like {likeCount}</button>
    </Form>
  );
};

In this example, we use the Form component with an onSubmit handler to optimistically update the UI. The handleSubmit function is called when the form is submitted, and it immediately updates the likeCount state, giving users an instant feedback.

Using the useFetcher Hook and Form Component from @remix-run/react Library

Another way to achieve Optimistic UI is by using the useFetcher hook and the Form component from the @remix-run/react library. Let's see an example:

import { Form, useFetcher, useLoaderData } from "@remix-run/react";
import type { ActionFunction, LoaderFunction } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { useState } from "react";

// LoaderFunction
export const loader: LoaderFunction = async () => {
  // Fetch data here
  return json({ likeCount: 0, postId: 1 });
};

// ActionFunction
export const action: ActionFunction = async ({ request }) => {
  // Perform action here, e.g., updating a like count

  return json({ status: "success" });
};

export const LikeButton = () => {
  const data = useLoaderData();
  const fetcher = useFetcher();

  const [likeCount, setLikeCount] = useState(data?.likeCount);
  const handleSubmit = async (e: any) => {
    
    // Update the UI optimistically
    setLikeCount(likeCount + 1);
    await fetcher.submit(e.target, { method: "post" });
  };

  return (
    <Form method="post" onSubmit={handleSubmit}>
      <input type="hidden" name="postId" value={data?.postId} />
      <button type="submit">Like {likeCount}</button>
    </Form>
  );
};

In this example, we use the useFetcher hook to send the request to the server after updating the UI optimistically. The handleSubmit function is called when the form is submitted, and it immediately updates the likeCount state, giving users an instant feedback. After updating the UI, the request is sent to the server using the fetcher.post() method.

Handling Errors with Remix and Optimistic UI

When implementing Optimistic UI, it's crucial to handle errors and revert the UI to its previous state if an action fails. Here's an example of how to handle errors with Remix and Optimistic UI:

import { Form, useFetcher, useLoaderData, useParams } from "@remix-run/react";
import type { ActionFunction, LoaderFunction } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { useEffect, useState } from "react";

// LoaderFunction
export const loader: LoaderFunction = async () => {
  // Fetch data here
};

// ActionFunction
export const action: ActionFunction = async ({ request }) => {
  // Perform action here, e.g., updating a like count

  // Simulate success or error
  const success = Math.random() > 0.5;
  if (success) {
    return json({ status: "success" });
  } else {
    return json({ status: "error", message: "Failed to update like count" });
  }
};

export const LikeButton = () => {
  const data = useLoaderData();
  const fetcher = useFetcher();
  const [likeCount, setLikeCount] = useState(data?.likeCount);
  const [error, setError] = useState(null);
  const params: any = useParams();

  const handleSubmit = async () => {
    // Update the UI optimistically
    const newLikeCount = likeCount + 1;
    setLikeCount(newLikeCount);

    // Send the request to the server
    fetcher.submit({ postId: params.postId }, { method: "post" });
  };

  useEffect(() => {
    if (fetcher.data.status === "error") {
      setError(fetcher.data.message);
      setLikeCount(likeCount - 1);
    }
  }, []);

  return (
    <>
      <Form onSubmit={handleSubmit}>
        <input type="hidden" name="postId" value={params.postId} />
        <button type="submit">Like {likeCount}</button>
      </Form>
      {error && <div className="error">{error}</div>}
    </>
  );
};

In this example, we handle errors by checking the response.status and reverting the UI to its previous state if the action fails. Additionally, we display an error message to inform the user about the failure.

In conclusion, Remix enables developers to easily implement Optimistic UI by leveraging the native Form component or the useFetcher hook from the @remix-run/react library. By incorporating error handling, developers can ensure a seamless user experience even when server-side issues occur.

Last updated