Using Shopify Polaris' navigation component with react-router
If you're building a (non-embedded) Shopify app, you'll have to implement your own navigation.
The Shopify Polaris React components are an excellent way to make your app look and feel like Shopify. They also provide a lot of common UI functionality, which speeds up the development process as you won't have to implement everything from scratch.
The navigation component is designed for this purpose:
The navigation component is used to display the primary navigation in the sidebar of the frame of an application. Navigation includes a list of links that merchants use to move between sections of the application.
It's easy to drop in if you're okay will full-page reloads, but if you're building a single-page app (SPA) using a router like react-router, the documentation is a little less clear.
Here's how I was able to get it working.
Routes component
Let's define some routes first:
// Routes.js
import React from "react";
import { Switch, Route } from "react-router-dom";
function Routes() {
return (
<Switch>
<Route path="/products">
<Products />
</Route>
<Route path="/settings">
<Settings />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
);
}
export default Routes;
Top-level app component
The app needs to be wrapped with certain providers, plus the router. A basic example could look like this (you will also need to pass linkComponent
to AppProvider
, which I've left out for simplicity, but you can learn how in the app provider docs):
// App.js
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { AppProvider } from "@shopify/polaris";
import translations from "@shopify/polaris/locales/en.json";
function App() {
return (
<BrowserRouter>
<AppProvider i18n={translations}>
{/* App content including routes */}
</AppProvider>
</BrowserRouter>
);
}
export default App;
Frame component
To use navigation, we first need to wrap the app with the frame component:
The frame component, while not visible in the user interface itself, provides the structure for an application. It wraps the main elements and houses the primary navigation, top bar, toast, and contextual save bar components.
The frame component actually takes the markup for various navigation elements as props, like this:
<Frame
topBar={topBarMarkup}
navigation={navigationMarkup}
showMobileNavigation={mobileNavigationActive}
onNavigationDismiss={toggleMobileNavigationActive}
skipToContentTarget={skipToContentRef.current}
>
{contextualSaveBarMarkup}
{loadingMarkup}
{pageMarkup}
{toastMarkup}
{modalMarkup}
</Frame>
We could try to put this in App.js
directly, but there's a problem with this: the navigation component takes a location
prop, which it uses to set the active menu item. We need that prop to be dynamic so we can update it when the URL changes, to keep the navigation in sync with the page route.
The easiest way to watch for location changes is to use the useLocation
hook from React Router. The problem is that useLocation
does not work outside the components wrapped by the router, so we can't use it in the root component. Let's create a new component to wrap the frame and its contents:
// AppFrame.js
import React from "react";
import { useLocation } from "react-router-dom";
import { Frame, Navigation } from "@shopify/polaris";
import Routes from "./Routes";
function AppFrame() {
const location = useLocation();
return (
<Frame
navigation={
<Navigation location={location.pathname}>
<Navigation.Section
items={[
{
url: "/",
label: "Dashboard",
},
{
url: "/products",
label: "Products",
},
{
url: "/settings",
label: "Settings",
},
]}
/>
</Navigation>
}
>
<Routes />
</Frame>
);
}
export default AppFrame;
Using the new AppFrame component
AppFrame
can now just be passed to the top-level component like this, and the navigation should work, thanks to the useLocation
hook:
// App.js
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { AppProvider } from "@shopify/polaris";
import translations from "@shopify/polaris/locales/en.json";
import AppFrame from "./AppFrame";
function App() {
return (
<BrowserRouter>
<AppProvider i18n={translations}>
<AppFrame />
</AppProvider>
</BrowserRouter>
);
}
export default App;
Making the navigation responsive
If you want your Shopify app to work on mobile, this implementation won't work for you quite yet. If you resize your browser window, you'll notice that the left navigation completely disappears on small screens.
Polaris provides a solution to this, but it requires you to use the top bar component:
The mobile version of the navigation component appears in the top bar component.
How do we integrate it into what we have so far?
Just like how we pass navigation markup to the frame component via the navigation
prop, there's also a topBar
prop that works the same way. So to add the top bar, we need to modify AppFrame
. We also need to handle the open/closed state of the top bar menu here. Here's how to do that:
// AppFrame.js
import React from "react";
import { useLocation } from "react-router-dom";
import { Frame, TopBar, Navigation } from "@shopify/polaris";
import Routes from "./Routes";
function AppFrame() {
const location = useLocation();
// Track the open state of the mobile navigation
const [mobileNavigationActive, setMobileNavigationActive] = React.useState(
false
);
const toggleMobileNavigationActive = React.useCallback(
() =>
setMobileNavigationActive(
(mobileNavigationActive) => !mobileNavigationActive
),
[]
);
return (
<Frame
showMobileNavigation={mobileNavigationActive}
onNavigationDismiss={toggleMobileNavigationActive}
topBar={
<TopBar
showNavigationToggle
onNavigationToggle={toggleMobileNavigationActive}
/>
}
navigation={
<Navigation location={location.pathname}>
<Navigation.Section
items={[
{
url: "/",
label: "Dashboard",
},
{
url: "/products",
label: "Products",
},
{
url: "/settings",
label: "Settings",
},
]}
/>
</Navigation>
}
>
<Routes />
</Frame>
);
}
export default AppFrame;
Customizing the top bar
The above example adds the most basic top bar, but you'll need to customize it to match your app's branding. You can change the color, add your logo, and configure things like extra menus and even a search bar.
To set the colors and logo, pass a static theme configuration object to AppProvider
.
To add extra elements and functionality, see the top bar docs for a thorough example of a fully customized top bar.