Updating browser URL on redirect

Hello,

I am having a little bit of hard time understanding how to programmatically manipulate the browser URL. I am still using Remix but with the new CDN version of app bridge.

The goal is to achieve the following:

  1. User submits a form to create a new resource at …/resources/new and the route’s action returns redirect(“…/resources/<created-resource-id“)
  2. User is redirected to .../resources/<created-resource-id>
  3. Browser URL is updated to https://…/resources/<created-resource-id>

Steps 1 and 2 are working just fine but I don’t understand how to update the browser URL to match the internal Remix route. Even after the redirect the browser URL is …/resouce/new and when I refresh the page it takes me back to the resource creation form instead of refreshing the resource index page.

The API replacements section of the app bridge migration docs say that the History and Redirect APIs have been replaced with shopify.navigation.navigate(), however when trying to use (or console log) the global shopify object, no such thing as navigation exists for it.

I have also added the

const navigate = useNavigate();

useEffect(() => {
  const handleNavigate = (event) => {
    const href = event.target.getAttribute(‘href’);
    if (href) navigate(href);
  };

  document.addEventListener('shopify:navigate', handleNavigate);
  return () => {
    document.removeEventListener('shopify:navigate', handleNavigate);
  };
}, [navigate]);

snippet in my app/root.tsx.

I do understand that navigating in Remix does not equal browser navigation since the app is inside an iframe but I would just like to understand what is the correct way of implementing my feature.

Thank you in advance!

Hi mmatila,

Thanks for reaching out. Can you confirm a couple things?

  1. In step 1 you should be throwing a redirect. It’s a quirk of Remix that it should be thrown vs just returned.
  2. What form component are you using? A plain HTML form, or something else?

The API replacements section of the app bridge migration docs say that the History and Redirect APIs have been replaced with shopify.navigation.navigate(), however when trying to use (or console log) the global shopify object, no such thing as navigation exists for it.

Thanks for catching this, it’s no longer correct. You should perform navigation using the browser Navigation API rather than anything on the Shopify object. Can you try that and let me know if it’s any different?

Hey,

thanks for the reply! To first answer your questions:

  1. Was not throwing but returning – honestly had no idea there’s a difference. Seems to be nothing in the internet related to this topic. Unfortunately this did not fix my issue though.
  2. I’m using the <Form /> from @remix-run/react. Though I am using through submit(…, { method: “post“ }) from Remix’ useSubmit instead of the “native” form submission. The reason being react-hook-form . I did also try with the native <form /> now that you mentioned it but no luck with that either.

So in short; the issue still persists but I will provide my setup below as thoroughly as possible. I’m sure the solution is simple and I’ve just misconfigured something or doing some weird shenanigans with my Remix & app bridge so please point out any weird patterns you might catch here. Slowly migrating away from @shopify/app-bridge-react and @shopify/polaris has also confused me a little so I might be mix-and-matching or misusing some APIs.

Below are the parts of my app relevant for this question. I’ve removed most of the unrelated code from each snippet for clarity.

app/root.tsx

import { useNavigate } from "@remix-run/react";

const App = () => {
	const navigate = useNavigate();

	useEffect(() => {
		const handleNavigate = (event: any) => {
			const href = event.target.getAttribute("href");
			if (href) navigate(href);
		};

		document.addEventListener("shopify:navigate", handleNavigate);
		return () => {
			document.removeEventListener("shopify:navigate", handleNavigate);
		};
	}, [navigate]);

	return (
		<html lang="en">
			<head>
				<meta name="shopify-api-key" content={SHOPIFY_API_KEY} />
				<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
				<script src="https://cdn.shopify.com/shopifycloud/polaris.js"></script>
			</head>
		</html>
	);
};

app.resources.new/route.tsx

import { Form, useSubmit } from "@remix-run/react";
import { useForm } from "react-hook-form";

export const action = async ({ request }) => {
  const { redirect } = await authenticate.admin(request);
  
  --- resource creation & session flash logic ---

  throw redirect(`/app/resources/${createdResourceId}`, { headers: { ... } });
}

const NewResourcePage = () => {
  const submit = useSubmit();
  const { handleSubmit } = useForm();

  const onSubmit = (formValues) => submit(formValues, { method: "post" });

  return (
    <Form onSubmit={handleSubmit(onSubmit)}>...</Form>
  );
}

app.resources.$id/route.tsx

import { useNavigate, useLoaderData } from "@remix-run/react";

export const loader = async ({ params, request }) => {
  const { redirect } = await authenticate.admin(request);
  
  --- resource fetching & getting session flash ---

  return Response.json({ resource, resourceCreated }, { headers: { ... } });
}

const ResourcePage = () => {
  const { resource, resourceCreated } = useLoaderData();
  const navigate = useNavigate();

  useEffect(() => {
    if (resourceCreated) {
      shopify.toast.show("Resource created!")

      // This is where I've tried to do all kinds of navigation things.
      // I've tried both taking the resource ID from params as well as
      // saving it in the session flash as value and taking it from there

      // Been keeping a very close eye on this as well
      // console.log(id)

      // No errors but nothing happens either.
      // navigate("/app/resources/<id>")

      // This gives an invalid URL error.
      // Source mapping points to a part in the app-bridge.js code where
      // shopify:navigation and location.href are manipulated.
      // window.navigation.navigate("/app/resources/<id>")

      // Hard coding some random id works (updates the browser URL).
      // navigate("/app/resources/123")
    }
  }, [resourceCreated])

  ...
}

So everything else works except updating the browser URL (and even this works if I hard code some random ID to the destination URL).

ChatGPT and Claude kept telling me the issue lies in the imperative submission of the form through submit(values, { method: "post" }) which led me to try this out without react-hook-form. That did not work either.

Please let me know if you see something alarming in the whole flow of things.

Thank you in advance!

After some more digging I came across this issue from the shopify-app-bridge repository.

The problem was not in the action of the app.resources.new route or useEffect of the app.resources.$id route. The problem was actually in the <ui-save-bar /> of the app.resources.new route.

To give more context here’s an extended version of my app.resources.new

import { Form, useSubmit } from "@remix-run/react";
import { useForm } from "react-hook-form";

export const action = async ({ request }) => {
  const { redirect } = await authenticate.admin(request);
  
  --- resource creation & session flash logic ---

  throw redirect(`/app/resources/${createdResourceId}`, { headers: { ... } });
}

const NewResourcePage = () => {
  const submit = useSubmit();
  const { handleSubmit, formState: { isDirty } } = useForm();

  const onSubmit = (formValues) => submit(formValues, { method: "post" });

  useEffect(() => {
    if (isDirty) {
      shopify.saveBar.show(SAVE_BAR_ID);
    } else {
      shopify.saveBar.hide(SAVE_BAR_ID)
    }
  }, [isDirty])

  return (
    <Form onSubmit={handleSubmit(onSubmit)}>
      <ui-save-bar id={SAVE_BAR_ID}>
        <button>...</button>
        <button>...</button>
      </ui-save-bar>

      <s-page>...</s-page>
    </Form>
  );
}

So I am programmatically controlling the <ui-save-bar /> visibility and for some reason this setup causes the save bar to block the redirection.

So the solution (or workaround) for my problem ended up being adding navigation.state !== 'loading' conditional to my useEffect.

import { useNavigation } from "@remix-run/react";

const navigation = useNavigation();

useEffect(() => {
  if (isDirty && navigation.state !== 'loading') { <-- Changed
    shopify.saveBar.show(SAVE_BAR_ID);
  } else {
    shopify.saveBar.hide(SAVE_BAR_ID);
  }
}, [isDirty, navigation.navigate]);

and everything started to work normal.

There could be a mention about this in the Save Bar docs maybe @Mitch_Lillie?

1 Like