How toRedirect to the original URL inside a Remix action

Let's say the user is currently at the URL /:username, and there's a button to follow that user (like in a Twitter profile page).

function View() {
  let { profile } = useRouteData<{ profile: { name: string, id: string }}>()
  return (
    <div>
      <h1>{profile.name}</h1>
      <Form method="post" action={`/users/${profile.name}/follow`}>
        <input type="hidden" value={profile.id} name="userId" />
        <button type="submit">Follow</button>
      </Form>
    </div>
  );
}

After the user submit our form inside our action we will receive the profile.id and then we need to redirect the user to another URL, but we want the user to redirect back to the URL the user was when it clicked the Follow button.

export let action: ActionFunction = async ({ request }) => {
  let body = await parseBody(request);
  let session = await getSession(request.headers.get("Cookie"));
  await followUser(body.get("userId"), session.get("token")); // imagine this create a new follow in the DB
  return redirect("?");
}

So how can we do this? There's a few ways.

Using the parameters

In that example in particular we can get the profile.name from the params, so we could do:

export let action: ActionFunction = async ({ request, params }) => {
  // the code from the first example, removed for simplicity
  return redirect(`/${params.name}`);
}

This will work in this case but it may not if the URL of the action was simply /like or something without any parameter in the middle

Pass the original URL inside the form

Another option is to send the URL we want to redirect the user to after the action inside the body of the action.

function View() {
  // we get the full URL from the loader, in the loader we can just return `request.url`
  let { profile, url } = useRouteData<{ profile: { name: string, id: string }, url: string }>()
  return (
    <div>
      <h1>{profile.name}</h1>
      <Form method="post" action={`/users/${profile.name}/follow`}>
        <input type="hidden" value={url} name="redirectTo" />
        <input type="hidden" value={profile.id} name="userId" />
        <button type="submit">Follow</button>
      </Form>
    </div>
  );
}

Then in the action:

export let action: ActionFunction = async ({ request, params }) => {
  // the code from the first example, removed for simplicity
  return redirect(body.get("redirectTo"));
}

And again that will work, we can be sure to always send that redirectTo value and this can be a nice pattern to follow everytime you need this.

Use the Referer HTTP header

The HTTP header Referer is a way the browser can send to a server the URL the user was coming from. We can get it from the request inside the action and use it to get the exact URL the user was before the submit.

export let action: ActionFunction = async ({ request, params }) => {
  // the code from the first example, removed for simplicity
  return redirect(request.headers.get("Referer"));
}

We can build a simple wrapper called redirectBack and pass the request and make the wrapper read the header for us. This is what I did for my redirectBack function in Remix Utils so you can just do:

export let action: ActionFunction = async ({ request, params }) => {
  // the code from the first example, removed for simplicity
  return redirectBack(request);
}

The main problem with this way of doing the redirect is the Referer header may not be present, e.g if the user defined the CSP header Referrer-Policy: no-referrer then from that page the browser will never send the Referer to any request. So for those cases we will need a fallback, the redirectBack function receives it as a second value:

export let action: ActionFunction = async ({ request, params }) => {
  // the code from the first example, removed for simplicity
  return redirectBack(request, { fallback: "/something" });
}

This way we can define a URL we are ok if the user is redirected to even if it's not the same, an example of this could be if we wanted to preserve search params we can use the Referer header to get them but as a fallback use the URL without search params.

If we combine this with the redirectTo inside the form body we can have something like this:

export let action: ActionFunction = async ({ request, params }) => {
  // the code from the first example, removed for simplicity
  return redirectBack(request, { fallback: body.get("redirectTo") });
}