Skip to content

Instantly share code, notes, and snippets.

@samselikoff
Created July 26, 2022 19:04
Show Gist options
  • Save samselikoff/510c020e4c9ec17f1cf76189ce683fa8 to your computer and use it in GitHub Desktop.
Save samselikoff/510c020e4c9ec17f1cf76189ce683fa8 to your computer and use it in GitHub Desktop.

Remix's useFetcher doesn't return a Promise for any of its methods (like fetcher.submit()) because Remix doesn't want you to explicitly await anything so they can handle things like cancellation for you. Instead, they recommend adding a useEffect and performing whatever logic you need to after the fetcher is in a particular state.

I found using an effect to run some logic after a submission to be too indirect, and there seem to be plenty of cases where you want to submit a form and then perform some other work on the client (sometimes async, like requesting the user's permission for their location), and I'd rather just do that after a submission in the event handler rather than an effect.

So here's a proof of concept hook that wraps Remix's useFetcher and returns a version of submit that is a promise, and resolves with the data from the action:

function useFetcherWithPromise() {
  let resolveRef = useRef();
  let promiseRef = useRef();
  if (!promiseRef.current) {
    promiseRef.current = new Promise((resolve) => {
      resolveRef.current = resolve;
    });
  }
  let fetcher = useFetcher();

  async function submit(...args) {
    fetcher.submit(...args);
    return promiseRef.current;
  }

  useEffect(() => {
    if (fetcher.data) {
      resolveRef.current(fetcher.data);
    }
  }, [fetcher]);

  return { ...fetcher, submit };
}

Now I can use it like this:

<fetcher.Form
  onSubmit={handleSubmit}
  className="mt-4 space-y-4"
  method="post"
>
  <!--  form  -->
</fetcher.Form>

and the event handler:

async function handleSubmit(event) {
  event.preventDefault();

  let data = await fetcher.submit(event.target, { method: "post" });
  // do additional work
  
  return navigate(`/exercises/${data.exerciseId}`);
}

Know that this subjects you to some pitfalls that Remix's Form is designed to protect you from, but again it seems to me there are plenty of cases where you'd want to drop down to this level.

@arunmmanoharan
Copy link

`
import type { FetcherWithComponents } from "@remix-run/react";
import { useFetcher } from "@remix-run/react";
import { useEffect, useState } from "react";
import type { AppData } from "@remix-run/react/dist/data";
import type { SerializeFrom } from "@remix-run/server-runtime";
import { resetFetcher } from "~/utils/resetFetcher";

/**
 * A higher-order function that creates a new FetcherWithComponentsPromise instance,
 * which extends the FetcherWithComponents interface.
 * The new instance includes an additional method `reset` that can be used to reset the state of the fetcher.
 *
 * @template T - The type of data returned by the fetcher.
 * @param fetcherWithComponents - The FetcherWithComponents instance to be extended.
 * @returns A new FetcherWithComponentsPromise instance.
 */
export type FetcherWithComponentsPromise<T> = FetcherWithComponents<T> & {
  reset: () => void;
  loadAsync: (href: string) => Promise<T>;
};

/**
 * Custom hook that wraps the useFetcher hook with the ability to reset data.
 *
 * @param {Object} opts - Optional options to pass to the useFetcher hook.
 * @returns {Object} - An object containing fetcher properties with added reset functionality.
 */
export function useFetcherWithPromise<T = AppData>(
  opts?: Parameters<typeof useFetcher>[0],
): FetcherWithComponentsPromise<SerializeFrom<T>> {
  const fetcher = useFetcher<T>({ ...opts, key: `${opts?.key}` });
  const [data, setData] = useState<SerializeFrom<T> | undefined>(fetcher.data);
  const [loadingPromise, setLoadingPromise] = useState<{
    resolve: (value: SerializeFrom<T>) => void;
    reject: (reason?: any) => void;
  } | null>(null);

  useEffect(() => {
    if (fetcher.state === "idle" && loadingPromise) {
      if (fetcher.data) {
        setData(fetcher.data);
        loadingPromise.resolve(fetcher.data as SerializeFrom<T>);
      } else {
        loadingPromise.reject(new Error("Failed to load data"));
      }
      setLoadingPromise(null);
    }
  }, [fetcher.state, fetcher.data, loadingPromise]);

  const loadAsync = (href: string) => {
    fetcher.load(href);
    return new Promise<SerializeFrom<T>>((resolve, reject) => {
      setLoadingPromise({ resolve, reject });
    });
  };

  return {
    ...fetcher,
    data: data as SerializeFrom<T>,
    loadAsync,
    reset: () => {
      setData(undefined);
      resetFetcher(fetcher);
    },
  };
}

`

This is my implementation. YOu can use fetcher.loadAsync('/resource-route').then()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment