How to get Shopify's Polaris to work with React routing packages

Unlike their API docs, Shopify's Polaris framework for React is a well documented package, replete with TypeScript definitions and usage examples. It's a real joy to use, and the suite of components it comes with cover almost everything you could possibly need to build a web application for Shopify's app platform. One of the really cool things about it is that you don't even need to have a Shopify app to use it; it works just as well in a generic web application as it does in a Shopify application.

However, if you are using Polaris in an embedded Shopify application scenario, there's one situation that they don't document very well: making it work with React routing packages like React Router or (my personal favorite) Navi.

Embedded Shopify applications are the same old web application you're probably already familiar with, but they're loaded in an iframe in a Shopify merchant's admin dashboard. The merchant never has to leave their Shopify store to use an embedded application, but the app itself remains in your control, hosted on your own servers.

Polaris tries to help with routing by letting you provide an arbitrary link component as a prop on their AppProvider component. The idea is you'll pass in a link component from your routing package, the user clicks on that link component, and your router handles the page change... but that link won't actually update the address bar in the merchant's browser. No matter which page they navigate to in your app, the address bar stays the same.

If the address bar in your user's browser isn't changing when they're using your app, navigation starts to behave in unexpected ways. The back button and forward buttons won't work the way a user would expect them to, they'd get taken back to another page in their admin dashboard rather than the last page they were on in your app. And if they hit the refresh button while using your app, they'd be taken back to your app's home page instead of the page they were on when they refreshed.

Obviously that's not ideal. You need a way to synchronize the URL that your application thinks its on with the URL displayed in the address bar. But since your app is loaded in an iframe, you don't have direct control of the user's history object or URL bar.

Now there are a few ways to make this work: if you look through the Polaris source code, or the source code for Shopify's embedded app SDK, you'll notice that there are several places where they're using a wacky iframe postmessage communication scheme to do things like display loading bars, pop up modals, and so on. But beyond loading bar and modal functionality, their iframe postmessage implementation also has listeners set up which let the embedded app communicate URL changes, so they can be synced with the browser and address bar.

That's all complicated, annoying and poorly documented, though, so I'm not actually going to get into their iframe postmessage stuff. Instead, let's just use the package that they've already written to do that for us! Sadly this package isn't documented anywhere (as far as I can tell, I only learned about it through this GitHub issue), but it does exactly what we need it to do.

Simply install the @shopify/react-shopify-app-route-propagator package from NPM, import it, and render it alongside your other React components:

import LocationSync from "@shopify/react-shopify-app-route-propagator";

...

render() {
    return (
        <div>
            <LocationSync app={polarisEmbeddedAppContext} location={"/current-url"} />
            ...
        </div>
    )
}

Whenever a React component is rendered with that LocationSync component, the user's address bar (and JavaScript history object) will sync up with the location string passed to that component. Assuming the location is "/current-url", then your user's address bar will be synced and display /current-url once the component mounts and renders.

If your routing package supports getting the current route during a render, you actually only need to add this component only once in your React app's entry point, rather than rendering it on every single page. For example, this is how I use the location sync with my favorite router package, Navi:

import LocationSyncComponent from "@shopify/react-shopify-app-route-propagator";
import { Router, CurrentRoute } from "react-navi";
import { AppProvider } from "@shopify/polaris";
import * as propTypes from "prop-types";
import * as React from "react";

class LocationSync extends React.Component {
    static contextTypes = {
        polaris: propTypes.object
    }

    render () {
        return (
            <CurrentRoute>
                {route => <LocationSyncComponent app={this.context.polaris.appBridge} location={route.url} /> } 
            </CurrentRoute>
        )
    }
}

export class Application extends React.Component {
    render() {
        return (
            <AppProvider
                apiKey={...}
                shopOrigin={...}
                forceRedirect={...}
                linkComponent={...}>
                <Router routes={...}>
                    <LoadingRoute>{loading => (loading ? <Loading /> : null)}</LoadingRoute>
                    <LocationSync />
                    <View />
                </Router>
            </AppProvider>
        );
    }
}

Note that the app prop that the LocationSync package wants comes from using the Polaris package. You need to set up a static contextTypes object on your React components to get the context during render. The location sync will not work without that context, you'll get a render error instead.


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.