Shopify / shopify-app-bridge

https://shopify.dev/docs/api/app-bridge
82 stars 9 forks source link

App Bridge 4 clicking on Navigation Menu causes app reload #240

Closed kirillplatonov closed 7 months ago

kirillplatonov commented 9 months ago

Describe the bug

When clicking on any link in Navigation Menu the app is fully reloaded to open new page. It starts loading from scratch with splash screen, token fetching, etc. This slows down loading dramatically and just looks weird for users.

To Reproduce

Steps to reproduce the behaviour:

  1. Define Navigation Menu with at least two pages:
    <ui-nav-menu>
    <a href="/first-page">First page</a>
    <a href="/second-page">Second page</a>
    </ui-nav-menu>
  2. Launch app
  3. Click on "First page" link
  4. Click on "Second page" link
  5. Notice full app reloading

https://github.com/Shopify/shopify-app-bridge/assets/839922/c521a87d-2492-4f3c-8901-55c025e11755

Expected behaviour

App Bridge should treat Navigation Menu clicks as in-app navigation. Same as navigating relative link within app: https://shopify.dev/docs/api/app-bridge-library/reference/navigation#example-navigating-to-routes-within-your-app

If this is not possible then at least we should have a way to add event handler and override Navigation Menu behaviour. This is how we've done it with App Bridge 3: https://github.com/kirillplatonov/shopify-hotwire-sample/blob/main/app/javascript/shopify_app/navigation.js#L25

Contextual information

Packages and versions

Platform

Additional context

Tech stack:

Rafi993 commented 9 months ago

I can see this issue too.

patryk-smc commented 9 months ago

I have the same issue. There is currently no way to subscribe to Redirect action. This is last thing that prevents me from migrating to the CDN version

MitchLillie commented 9 months ago

Hello, thanks for your report.

Can you try specifying your app's root path as the first item in the nav menu, and adding rel="home" to that link? This is what our docs specify.

For example:

<ui-nav-menu>
  <a href="/" rel="home">Home</a>
  <a href="/first-page">First page</a>
  <a href="/second-page">Second page</a>
</ui-nav-menu>
Rafi993 commented 9 months ago

Hello, thanks for your report.

Can you try specifying your app's root path as the first item in the nav menu, and adding rel="home" to that link? This is what our docs specify.

For example:

<ui-nav-menu>
  <a href="/" rel="home">Home</a>
  <a href="/first-page">First page</a>
  <a href="/second-page">Second page</a>
</ui-nav-menu>

That doesn't seem to work. I think the page has to be cached like it was done in the previous app bridge

asacarter commented 9 months ago

Use react Link to="" instead of <a href=""

bithaolee commented 9 months ago

We are experiencing the same problem, please Shopify considers our users who don't use Remix, I looked at the original app bridge react part of the code and there is a CustomRouter defined in there to synchronize the paths between shopify and the app, how can we implement this please

Rafi993 commented 9 months ago

Use react Link to="" instead of <a href=""

Thanks. This actually works!!

bithaolee commented 9 months ago

Use react Link to="" instead of <a href=""

Thank you very much. It really works. image

kirillplatonov commented 8 months ago

This doesn't solve the problem for non-react apps. We still need a proper way to make it work with normal links without hacks. Same way as Navigation component works: https://shopify.dev/docs/api/app-bridge-library/reference/navigation#example-navigating-to-routes-within-your-app

paweltatarczuk commented 8 months ago

You can do this in plain JavaScript. The key is to prevent browser's default behaviour with event.preventDefault(). Something like this should work:

<ui-nav-menu>
  <a href="/" rel="home" onclick="navigate">Home</a>
  <a href="/first-page" onclick="navigate">First page</a>
  <a href="/second-page" onclick="navigate">Second page</a>
</ui-nav-menu>

<script>
function navigate(event) {
  event.preventDefault();
  history.pushState({}, null, event.target.href);
}
</script>
kirillplatonov commented 8 months ago

Yeah, it's true. We doing something similar for Turbo as well. But it's still broken out of the box and needs to be fixed.

awd commented 8 months ago

You can do this in plain JavaScript. The key is to prevent browser's default behaviour with event.preventDefault(). Something like this should work:

<ui-nav-menu>
  <a href="/" rel="home" onclick="navigate">Home</a>
  <a href="/first-page" onclick="navigate">First page</a>
  <a href="/second-page" onclick="navigate">Second page</a>
</ui-nav-menu>

<script>
function navigate(event) {
  event.preventDefault();
  history.pushState({}, null, event.target.href);
}
</script>

Is this actually working for you? I've just tested it out and there isn't any difference. The AppBridge navigation is still sending unauthenticated requests into the app - causing page reloading.

It's definitely a hack and behaviour could easily break if not part of AppBridge by default - so I would not advise caution to anyone reading into this.

@kirillplatonov has the right sentiment here - AppBridge should send these requests with the Bearer token provided, so the app can simply load the view without reloading/redirecting.

paweltatarczuk commented 8 months ago

Yes, it works for me and no, it's not a hack. This is what React Router Link is doing under the hood.

kirillplatonov commented 8 months ago

This code only updates browser URL without navigating to specific page:

history.pushState({}, null, event.target.href);

CleanShot 2023-10-15 at 08 23 58@2x

It's suitable as a fix for #242 when you need to keep focus on the right menu element. But not as a fix for navigation itself.

paweltatarczuk commented 8 months ago

I incorrectly assumed that the example app is an SPA. In this case dynamic links won't help as they would require some kind of router that would update the page accordingly.

Maybe it would be possible to workaround this issue with View Transitions API but it's just a guess. I hope app bridge gets a fix for static links in the Navigation component.

iamphonghg commented 7 months ago

For those using React, @shopify/app-bridge 3.4.3, react-router-dom 5.3.0. I also encountered the app reloading when clicking on a navigation item until I used this code on the AppBridgeProvider: app.subscribe(Redirect.Action.APP, function() {});

henrytao-me commented 7 months ago

Hi everyone, I have a few notes about this:

With the new App Bridge via Shopify CDN

<ui-nav-menu>
  <a href="/first-page">First page</a>
  <a href="/second-page">Second page</a>
</ui-nav-menu>
<ui-nav-menu>
  <a href="/first-page">First page</a>
  <a href="/second-page">Second page</a>
</ui-nav-menu>

// In Javascript
document.querySelector('ui-nav-menu').addEventListener('click', (e) => {
  if (e.target.nodeName === 'A') {
     // handle your own redirect here
      e.preventDefault();
  }
});

With all older version of App Bridge

awd commented 7 months ago

Hi @henrytao-me

I've tried adding the click event listener, and have confirmed the ui-nav-menu is found in the DOM, but the click event is not firing. I suspect the version on this CDN is not the latest?

PhiloNL commented 6 months ago

Hi @henrytao-me

I'm having trouble with the event listener as well. It only seems to trigger when clicking back and forth.

https://github.com/Shopify/shopify-app-bridge/assets/1133950/b82011f7-e794-47c9-9a2d-668aef965e5c

Update Managed to get it working by adding history.pushState({}, null, event.target.href); after handling the redirect.

daviareias commented 5 months ago

Use react Link to="" instead of <a href=""

Where is this component front? from polaris doesn't have a to field anymore.

sebastianpisula commented 3 months ago

https://polaris.shopify.com/components/utilities/app-provider?example=app-provider-with-link-component

henrytao-me commented 3 months ago

Hi all, I just want to follow up on this issue. Let's me know if anyone is still having issue with this.

TL;DR about how App Bridge CDN handles redirect

sebastianpisula commented 3 months ago

It was helpful for me

Use react Link to="" instead of <a href=""

Thank you very much. It really works. image

jbeladiya commented 3 months ago

Hi all, I just want to follow up on this issue. Let's me know if anyone is still having issue with this.

TL;DR about how App Bridge CDN handles redirect

Hi @henrytao-me,

For single-page apps, manually implementing redirection may work, but afterward, if you click the browser's back button, it might not redirect properly. This issue could occur because the URL is not signed with HMAC. Please let me know if there is a solution for this problem.

tobiasdalhof commented 3 months ago

Vue 3 example:

<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'

const router = useRouter()

function navigate(event: MouseEvent) {
  const pathname = (event.target as HTMLAnchorElement).pathname
  history.pushState({}, '', pathname)
  router.replace(pathname)
}

// Re-render ui-nav-menu on route change
const key = ref(0)
watch(router.currentRoute, () => key.value++)
</script>

<template>
  <ui-nav-menu :key="key">
    <a href="/" rel="home" @click.prevent="navigate">Home</a>
    <a href="/page-1" @click.prevent="navigate">Page 1</a>
    <a href="/page-2" @click.prevent="navigate">Page 2</a>
  </ui-nav-menu>
</template>
jbeladiya commented 3 months ago

Vue 3 example:

<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

const router = useRouter()
const { t } = useI18n()

function navigate(event: MouseEvent) {
  const pathname = (event.target as HTMLAnchorElement).pathname
  history.pushState({}, '', pathname)
  router.replace(pathname)
}

// Re-render ui-nav-menu on route change
const key = ref(0)
watch(router.currentRoute, () => key.value++)
</script>

<template>
  <ui-nav-menu :key="key">
    <a href="/" rel="home" @click.prevent="navigate">{{ t('home') }}</a>
    <a href="/page-1" @click.prevent="navigate">{{ t('page-1') }}</a>
    <a href="/page-2" @click.prevent="navigate">{{ t('page-2') }}</a>
  </ui-nav-menu>
</template>

Hi @tobiasdalhof, The solution provided doesn't work for me. Additionally, I have a React app, and my code is as follows:

import { useNavigate } from "react-router-dom"; // v6

const navigate = useNavigate();

const pageRedirect = useCallback((e) => {
    e.preventDefault();
    window.history.pushState({}, '', e.target.href);
    navigate(e.target.pathname);
}, [navigate]);

<NavMenu>
        {Object.values(shopifyNavigation).map((i, index) => {
          return (
            <a
              href={i.destination}
              key={index}
              onClick={pageRedirect}
            >
              {i.label}
            </a>
          );
        })}
</NavMenu> 

Here, I'm loading all nav-items dynamically

sebastianpisula commented 3 months ago

@jbeladiya , example from my project:


import {NavMenu} from "@shopify/app-bridge-react";
import {Link} from "react-router-dom";

<NavMenu>
            <Link to="/">General settings</Link>
            <Link to="/faq">FAQ</Link>
            <Link to="/support">Support</Link>
            <Link to="/plans-and-pricing">Plans and pricing</Link>
        </NavMenu>
jbeladiya commented 3 months ago
General settings

@sebastianpisula, approach is same. When I click on a navigation item, I am redirected to a specific route with that particular item highlighted. However, I am facing an issue because in my app, I have a back button to redirect to the previous screen or a specific route. When I click on the back button, it navigates to the specific route, but the corresponding navigation item is not highlighted at that time.

https://github.com/Shopify/shopify-app-bridge/assets/120442287/c6d6782d-7e95-425a-8166-d9f88e3c824c

umutnacak commented 1 month ago

Hi @jbeladiya If you didn't already solve your issue here is a code snippet for you to try:

import { NavMenu } from "@shopify/app-bridge-react";
import { useMemo } from "react";
import { useLocation, Link } from "react-router-dom";

const location = useLocation();
const navMenu = useMemo(() =>
<NavMenu key={location.key}>
  {Object.values(shopifyNavigation).map((i, index) => {
    return (
      <Link to={i.destination}>
        {i.label}
      </Link>
    );
  })}
</NavMenu>, [location, shopifyNavigation]);

{ navMenu }

I didn't test with dynamic values so am not sure if it works like this, but here is a simple snippet that I already use in production:

import { NavMenu } from "@shopify/app-bridge-react";
import { useMemo } from "react";
import { useLocation, Link } from "react-router-dom";

const location = useLocation();
const navMenu = useMemo(() =>
<NavMenu key={location.key}>
  <Link to="/" rel="home">
    Home
  </Link>
  <Link to="/page1">
    Page1
  </Link>
  <Link to="/page2">
    Page2
  </Link>
</NavMenu>, [location]);

{ navMenu }

Hope this helps future readers.

jbeladiya commented 1 month ago

Hi @jbeladiya If you didn't already solve your issue here is a code snippet for you to try:

import { NavMenu } from "@shopify/app-bridge-react";
import { useMemo } from "react";
import { useLocation, Link } from "react-router-dom";

const location = useLocation();
const navMenu = useMemo(() =>
<NavMenu key={location.key}>
  {Object.values(shopifyNavigation).map((i, index) => {
    return (
      <Link to={i.destination}>
        {i.label}
      </Link>
    );
  })}
</NavMenu>, [location, shopifyNavigation]);

{ navMenu }

I didn't test with dynamic values so am not sure if it works like this, but here is a simple snippet that I already use in production:

import { NavMenu } from "@shopify/app-bridge-react";
import { useMemo } from "react";
import { useLocation, Link } from "react-router-dom";

const location = useLocation();
const navMenu = useMemo(() =>
<NavMenu key={location.key}>
  <Link to="/" rel="home">
    Home
  </Link>
  <Link to="/page1">
    Page1
  </Link>
  <Link to="/page2">
    Page2
  </Link>
</NavMenu>, [location]);

{ navMenu }

Hope this helps future readers.

Hi @underwoodsimon it's work for me. Thanks. But other side browser back not working properly when I click on browser back two or three time then after it's redirect to previous route. If you have also solution for it let me know.

umutnacak commented 1 month ago

@jbeladiya I also faced that issue during development, most likely because react renders twice in strict mode. So probably there are multiple entries of the same location in the browser history and you need to click twice or more on the back button. You need to test on production to see if there is a difference.

liliangrong777 commented 1 month ago

Use react Link to="" instead of <a href=""

👍

bithaolee commented 1 month ago

This code only updates browser URL without navigating to specific page:

history.pushState({}, null, event.target.href);

CleanShot 2023-10-15 at 08 23 58@2x

It's suitable as a fix for #242 when you need to keep focus on the right menu element. But not as a fix for navigation itself.

I'm trying to switch pages via navigate and change the active menu in the left menus, but I can't seem to get it to work, I'm using open('/path', '_self') but it causes the app to be reloaded, there's Any solution?