简体   繁体   中英

Client-side routing in an embedded shopify app

I'm trying to enable client-side routing using react-router inside an embedded shopify app. I used this guidline from Shopify but I don't clearly understand what should be in a ./Routes file. I tried to search for similar questions on Stack overflow or blogs, but information there is a bit outdated.

My code: App.jsx:

import {
  ApolloClient,
  ApolloProvider,
  HttpLink,
  InMemoryCache,
} from "@apollo/client";
import {
  Provider as AppBridgeProvider,
  useAppBridge,
} from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { Redirect } from "@shopify/app-bridge/actions";
import { AppProvider as PolarisProvider } from "@shopify/polaris";
import translations from "@shopify/polaris/locales/en.json";
import "@shopify/polaris/build/esm/styles.css";

import {useMemo} from 'react';
import {useLocation, useNavigate, BrowserRouter, RouterProvider} from 'react-router-dom';

import { HomePage } from "./components/HomePage";
import Routes from './Routes';

function App() {
  const navigate = useNavigate();
  const location = useLocation();
  const history = useMemo(
    () => ({replace: (path) => navigate(path, {replace: true})}),
    [navigate],
  );
  console.log(new URLSearchParams(document.location.search).get("host"))  
  const router = useMemo(
    () => ({
      location,
      history,
    }),
    [location, history],
  );
  return (
    <PolarisProvider i18n={translations}>
      <AppBridgeProvider
        config={{
          apiKey: process.env.SHOPIFY_API_KEY,
          host: new URLSearchParams(document.location.search).get("host"),
          forceRedirect: true,
        }}
        router={router}
      >
        <MyProvider>
            <Routes />
        </MyProvider>
      </AppBridgeProvider>
    </PolarisProvider>
  );
}

function MyProvider({ children }) {
  const app = useAppBridge();

  const client = new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      credentials: "include",
      fetch: userLoggedInFetch(app),
    }),
  });

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

export function userLoggedInFetch(app) {
  const fetchFunction = authenticatedFetch(app);

  return async (uri, options) => {
    const response = await fetchFunction(uri, options);

    if (
      response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1"
    ) {
      const authUrlHeader = response.headers.get(
        "X-Shopify-API-Request-Failure-Reauthorize-Url"
      );

      const redirect = Redirect.create(app);
      redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
      return null;
    }

    return response;
  };
}

export default function AppWrapper() {
  return (
    <BrowserRouter>
      <App />
    </BrowserRouter>
  );
}

./Routes.jsx

import React from 'react';
import {createBrowserRouter} from "react-router-dom";
import { HomePage } from './components/HomePage';
import { Instructions } from './components/Instructions';

export default function Routes() { 
    const routes = createBrowserRouter([
        {
        path: "/",
        element: <HomePage />,
        },
        {
            path: "/statistics",
            element: <HomePage />,
        },
        {
            path: "/integration",
            element: <Instructions />,
        }
    ]);

    return  routes
}

Any help is massively appreciated:-)

I think the important note from the docs is this:

If you are using React Router, ensure that the Provider is a child of the router component.

Code:

 export function MyApp() { const config = { apiKey: '12345', host: host }; const navigate = useNavigate(); const location = useLocation(); const history = useMemo( () => ({ replace: (path) => navigate(path, { replace: true })}), [navigate], ); const router = useMemo( () => ({ location, history, }), [location, history], ); return ( <Provider config={config} router={router} > <Routes /> </Provider> ); } export default function AppWrapper() { return ( <BrowserRouter> // <-- Router wraps App <App /> </BrowserRouter> ); }

This is because the App component necessarily needs a routing context provided to it from higher in the ReactTree so it can use the useNavigate and useLocation hook and proxy a history object to the navigate function.

At this point the Routes component can be any React component that renders some routes to be matched and rendered. For your case you can't use createBrowserRouter and define the router below the App component that is trying to consume it. Use the older BrowserRouter as in the AppWrapper example above, and Routes is a component that renders the routes you want.

Example

import React from 'react';
import { useRoutes } from "react-router-dom";
import { HomePage } from './components/HomePage';
import { Instructions } from './components/Instructions';

export default function Routes() { 
  const routes = useRoutes([
    { path: "/", element: <HomePage /> },
    { path: "/statistics", element: <HomePage /> },
    { path: "/integration", element: <Instructions /> }.
  ]);

  return routes;
}

...

...
import Routes from './Routes';

function App() {
  const navigate = useNavigate();
  const location = useLocation();

  const history = useMemo(
    () => ({replace: (path) => navigate(path, {replace: true})}),
    [navigate],
  );

  const router = useMemo(
    () => ({
      location,
      history,
    }),
    [location, history],
  );

  return (
    <PolarisProvider i18n={translations}>
      <AppBridgeProvider
        config={{
          apiKey: process.env.SHOPIFY_API_KEY,
          host: new URLSearchParams(document.location.search).get("host"),
          forceRedirect: true,
        }}
        router={router}
      >
        <MyProvider>
          <Routes />
        </MyProvider>
      </AppBridgeProvider>
    </PolarisProvider>
  );
}

Alternative

If you want to use the new react-router-dom@6.4 Data APIs then the following refactor might work for you. Create the new data-connected BrowserRouter but refactor the routes such that App is rendered as a layout route ( ie it is rendered on a Route and itself renders an Outlet for the nested routes to render their content into ).

Example:

App

...
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
...

function App() {
  const navigate = useNavigate();
  const location = useLocation();

  const history = useMemo(
    () => ({replace: (path) => navigate(path, {replace: true})}),
    [navigate],
  );

  const router = useMemo(
    () => ({
      location,
      history,
    }),
    [location, history],
  );

  return (
    <PolarisProvider i18n={translations}>
      <AppBridgeProvider
        config={{
          apiKey: process.env.SHOPIFY_API_KEY,
          host: new URLSearchParams(document.location.search).get("host"),
          forceRedirect: true,
        }}
        router={router}
      >
        <MyProvider>
          <Outlet />
        </MyProvider>
      </AppBridgeProvider>
    </PolarisProvider>
  );
}

export default App;

Create the Router .

import React from 'react';
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from './path/to/App';
import { HomePage } from './components/HomePage';
import { Instructions } from './components/Instructions';

const router = createBrowserRouter([
  {
    path="/",
    element={<App />}
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "/statistics",
        element: <HomePage />,
      },
      {
        path: "/integration",
        element: <Instructions />,
      }
    ],
  },
]);

export default function Router() {
  return <RouterProvider router={router} />;
}

...

Import and render the router now:

import Router from '..path/to/Router';

...

<Router />

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM