Skip to content

Upgrading from Remix

Richard Powell edited this page Oct 1, 2025 · 27 revisions

Upgrading to React Router from Remix

TLDR

  • What is released?: A new package & template
  • Polaris web components: RR package is all-in on Polaris web components. These work nicely with React.
  • A simpler package: REST, non-embedded and AppProxyForm have low adoption and won't be ported from Remix.

Why upgrade

It's very similar: The RR package is a fork of the Remix package and the most commonly used API's are not changing.

Detailed migration: We're detailing each step and providing example migration branches. As we migrate more apps we'll improve this guide even more.

Polaris Web Components: The RR package uses Polaris Web Components, which automatically match Shopify's admin design and update as the platform evolves. This provides merchants with a consistent experience, reduces your maintenance overhead, and allows you to focus on core business functionality. These components share APIs with other Shopify surfaces, simplifying development.

Limited Remix V3 Support: The RR template will be prioritized over the Remix template. Support for the Remix package in its current state will be limited to critical security issues.

We're removing some features

Some features with very low adoption are being removed from the RR package. Don't worry: Most Remix apps don't use these features and there are alternatives. These features will still be supported by Shopify, they just won't be part of the RR package.

REST

If you don't use REST you can skip this section.

We previously communicated we are all in on GraphQL and added a future flag to the Remix package to remove REST. We have removed REST from the RR package.

You can either migrate to GraphQL, or use the REST client directly.

Most Remix apps do not use REST and are unaffected by this change.

Non-embedded support

If you don't know what this means, your app is probably embedded. You can skip this section if your app is embedded.

Around 2% of Remix apps render in a separate tab or window (Non-embedded). This merchant UX is not as good so we are dropping support for this in the RR package. If your app is not embedded you can migrate to embedded, or stay on the Remix package.

Around 98% of Remix apps are unaffected by this change.

Auth-code flow

If your app was created after January 25th 2024 and remained embedded, or if your adopted the unstable_newEmbeddedAuthStrategy future flag you can skip this section.

Let's explain 2 auth strategies:

If your app is embedded, you should enable the unstable_newEmbeddedAuthStrategy flag before migrating to RR. If your app is not embedded, please see the previous section.

Most Remix apps are unaffected by this change.

Remove AppProxyForm

If your app does not use AppProxyForm, you can skip this section.

If your app uses AppProxyForm, you should switch to using lowercase <form>, rather than <AppProxyForm/>. If this change causes you issues please provide feedback so we can understand your use case. There may be better solutions like Customer Account Extensions.

A very small percent of Remix apps are affected by this change.

Starting a new RR Shopify app

You can create a new Shopify app that uses React Router using this command:

shopify app init --template=https://github.com/Shopify/shopify-app-template-react-router

Please see the CLI documentation for more information.

Migrating a Remix Shopify app

If you prefer to see all the changes we've prepared 2 migration branches:

Detailed instructions follow below. If you encounter issues please provide feedback.

Adopt Vite

If your app was created after February 21st 2024, or you have already adopted Vite, you can skip this section.

If your app was created before February 21st 2024, you may need to adopt Vite. Here are some resources to follow:

Adopt future flags

These steps are described in the official Remix future flags guide.

Depending on when your app was created, you may be able to skip some of these steps:

  1. Remove installGlobals:

    vite.config.ts

    - import { installGlobals } from "@remix-run/node";
    - installGlobals({ nativeFetch: true });

    package.json

     "engines": {
    -    "node": "^18.20 || ^20.10 || >=21.0.0"
    +    "node": ">=20.10"
     },

    A commit showing these changes

  2. Update the tsconfig:

    tsconfig.json

    -  "types": ["node"]
    +  "types": ["@remix-run/node", "vite/client"]

    A commit showing these changes

  3. Adopt v3_singleFetch:

    vite.config.ts

     remix({
        ignoredRouteFiles: ["**/.*"],
        future: {
          v3_fetcherPersist: true,
          v3_relativeSplatPath: true,
          v3_throwAbortReason: true,
          v3_lazyRouteDiscovery: true,
    +     v3_singleFetch: true,
          v3_routeConfig: true,
        },
      })

    IMPORTANT: Single fetch changes how Response headers are managed. Because Shopify app's frequently send Responses with specific headers you must make sure to always include a headers export in every route that calls authenticate.admin(request).

    /app/routes/**.tsx

    + import { boundary } from "@shopify/shopify-app-remix/server";
    + import type { HeadersFunction } from "@remix-run/node";
    
     export const loader = async ({ request }: LoaderFunctionArgs) => {
       await authenticate.admin(request);
       return null;
     };
    
    + export const headers: HeadersFunction = (headersArgs) => {
    +   return boundary.headers(headersArgs);
    + };

    A commit showing these changes

  4. If your app uses json() in loaders or actions you should remove that now:

    app/routes/**/*.tsx:

    - import { json } from "@remix-run/node";
    
    export const loader = async ({ request }) => {
    
    -  return json({some: "data"})
    +  return {some: "data"}
    }
    
    export const action = async ({ request }) => {
    
    -  return json({some: "data"})
    +  return {some: "data"}
    }

    Make sure to remove replace return json(data) with return datain every loader or action insideapp/routes/`

  5. (Optional) Update eslint config by replacing its content:

    Here is the .eslintrc.cjs we recommend:

    /** @type {import('eslint').Linter.Config} */
    module.exports = {
      root: true,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        ecmaFeatures: {
          jsx: true,
        },
      },
      env: {
        browser: true,
        commonjs: true,
        es6: true,
      },
      ignorePatterns: ['!**/.server', '!**/.client'],
    
      // Base config
      extends: ['eslint:recommended'],
    
      overrides: [
        // React
        {
          files: ['**/*.{js,jsx,ts,tsx}'],
          plugins: ['react', 'jsx-a11y'],
          extends: [
            'plugin:react/recommended',
            'plugin:react/jsx-runtime',
            'plugin:react-hooks/recommended',
            'plugin:jsx-a11y/recommended',
          ],
          settings: {
            react: {
              version: 'detect',
            },
            formComponents: ['Form'],
            linkComponents: [
              {name: 'Link', linkAttribute: 'to'},
              {name: 'NavLink', linkAttribute: 'to'},
            ],
            'import/resolver': {
              typescript: {},
            },
          },
        },
    
        // Typescript
        {
          files: ['**/*.{ts,tsx}'],
          plugins: ['@typescript-eslint', 'import'],
          parser: '@typescript-eslint/parser',
          settings: {
            'import/internal-regex': '^~/',
            'import/resolver': {
              node: {
                extensions: ['.ts', '.tsx'],
              },
              typescript: {
                alwaysTryTypes: true,
              },
            },
          },
          extends: [
            'plugin:@typescript-eslint/recommended',
            'plugin:import/recommended',
            'plugin:import/typescript',
          ],
        },
    
        // Node
        {
          files: ['.eslintrc.cjs'],
          env: {
            node: true,
          },
        },
      ],
    };

    This commit shows these changes

  6. (Optional) Update the tsconfig

    Here is the tsconfig we recommend:

    {
      "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"],
      "compilerOptions": {
        "lib": ["DOM", "DOM.Iterable", "ES2019"],
        "isolatedModules": true,
        "esModuleInterop": true,
        "jsx": "react-jsx",
        "moduleResolution": "node16",
        "resolveJsonModule": true,
        "target": "ES2019",
        "strict": true,
        "allowJs": true,
        "checkJs": true,
        "noImplicitAny": false,
        "forceConsistentCasingInFileNames": true,
        "baseUrl": ".",
        "paths": {
          "~/*": ["./app/*"]
        },
        "types": [
          "@shopify/app-bridge-types"
        ],
        "noEmit": true
      }
    }

Upgrade to React Router

These steps are described in the official upgrade from Remix guide.

  1. Swap to React Router. First run the codemod:

    npx codemod remix/2/react-router/upgrade && npm install

    Make sure app/routes.ts is configured correctly:

    import { flatRoutes } from "@react-router/fs-routes";
    
    export default flatRoutes();

    Alternatively you can migrate to declaring routes explicitly.

    Now update the reactRouter() config to remove future flags:

    vite.config.ts

    - reactRouter({
    -   ignoredRouteFiles: ["**/.*"],
    -   future: {
    -     v3_fetcherPersist: true,
    -     v3_relativeSplatPath: true,
    -     v3_throwAbortReason: true,
    -     v3_lazyRouteDiscovery: true,
    -     v3_singleFetch: true,
    -     v3_routeConfig: true,
    -   },
    - }),
    + reactRouter()

    A commit showing these changes, most of which are created by the codemod.

  2. Tell the Shopify CLI how to build your app.

    shopify.web.toml

    - name = "remix"
    + name = "React Router"
    
    [commands]
    predev = "npx prisma generate"
    - dev = "npx prisma migrate deploy && npm exec remix vite:dev"
    + dev = "npx prisma migrate deploy && npm exec react-router dev"

    A commit showing these changes

  3. React Router can automatically generate types for your routes. Let's make sure the app is setup for that now:

    .gitignore

    + # Hide files auto-generated by react router
    + .react-router/

    env.d.ts

    - /// <reference types="@remix-run/node" />
    + /// <reference types="@react-router/node" />

    tsconfig.json

    -  "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],
    +  "include": ["env.d.ts", "**/*.ts", "**/*.tsx", ".react-router/types/**/*"],
      "compilerOptions": {
    -    "types": ["@remix-run/node", "vite/client"]
    +    "types": ["@react-router/node", "vite/client"],
    +    "rootDirs": [".", "./.react-router/types"]
      }

    A commit showing these changes

Swap shopify app remix

Now that React Router is set up it's time to swap @shopify/shopify-app-remix for @shopify/shopify-app-react-router.

  1. Swap dependencies:

    package.json

    - "@shopify/shopify-app-remix": "^3.7.0",
    + @shopify/shopify-app-react-router": "0.1.1",

    app/shopify.server.ts

    - import "@shopify/shopify-app-remix/adapters/node";
    - import { ApiVersion, AppDistribution, shopifyApp} from "@shopify/shopify-app-remix/server";
    + import "@shopify/shopify-app-react-router/adapters/node";
    + import { ApiVersion, AppDistribution, shopifyApp} from "@shopify/shopify-app-react-router/server";

    app/routes/**.tsx

    - import { boundary } from "@shopify/shopify-app-remix/server";
    + import { boundary } from "@shopify/shopify-app-react-router/server";

    app/routes/auth.login/error.server.tsx

    - import type { LoginError } from "@shopify/shopify-app-remix/server";
    - import { LoginErrorType } from "@shopify/shopify-app-remix/server";
    + import type { LoginError } from "@shopify/shopify-app-react-router/server";
    + import { LoginErrorType } from "@shopify/shopify-app-react-router/server";

    A commit showing these changes

  2. The React Router package uses App Bridge and Polaris Web Components, rather than Polaris React. Let's configure App Bridge and Polaris Web Components:

    app/routes/app.tsx

    - import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
    - export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
    
    - <AppProvider isEmbeddedApp apiKey={apiKey}>
    + <AppProvider embedded apiKey={apiKey}>

    Make sure to include any custom code you added to app.tsx like NavMenu items.

    app/routes/auth.login/route.tsx

    import {
    - AppProvider as PolarisAppProvider,
      Button,
      Card,
      FormLayout,
      Page,
      Text,
      TextField,
    } from "@shopify/polaris";
    - import polarisTranslations from "@shopify/polaris/locales/en.json";
    - import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
    + import { AppProvider } from "@shopify/shopify-app-react-router/react";
    
    - export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
    
    export const loader = async ({ request }: LoaderFunctionArgs) => {
      const errors = loginErrorMessage(await login(request));
    -  return { errors, polarisTranslations };
    +  return { errors };
    };
    
    export default function Auth() {
      return (
    -   <PolarisAppProvider i18n={loaderData.polarisTranslations}>
    +   <AppProvider embedded={false}>
       
    -   </PolarisAppProvider>
    +   </AppProvider>
     );
    }

    package.json

    "dependencies": {
    + "@shopify/app-bridge-ui-types": "^0.1.1",
    },

    A commit that shows these changes

Upgrade your UI to Polaris Web Components

You are ready to upgrade your UI to Polaris Web Components.

  1. Update the login route:

    app/routes/auth.login/route.tsx:

    - import {
    -  Button,
    -  Card,
    -  FormLayout,
    -  Page,
    -  Text,
    -  TextField,
    - } from "@shopify/polaris";
    
    export default function Auth() {
      const loaderData = useLoaderData<typeof loader>();
      const actionData = useActionData<typeof action>();
      const [shop, setShop] = useState("");
      const { errors } = actionData || loaderData;
    
      return (
        <AppProvider embedded={false}>
    -     <Page>Add commentMore actions
    -       <Card>
    -         <Form method="post">
    -           <FormLayout>
    -             <Text variant="headingMd" as="h2">
    -               Log in
    -             </Text>
    -             <TextField
    -               type="text"
    -               name="shop"
    -               label="Shop domain"
    -               helpText="example.myshopify.com"
    -               value={shop}
    -               onChange={setShop}
    -               autoComplete="on"
    -               error={errors.shop}
    -             />
    -             <Button submit>Log in</Button>
    -           </FormLayout>
    -         </Form>
    -       </Card>
    -     </Page>
    +     <s-page>
    +       <Form method="post">
    +         <s-section heading="Log in">
    +           <s-text-field
    +             type="text"
    +             name="shop"
    +             label="Shop domain"
    +             helpText="example.myshopify.com"
    +             value={shop}
    +             onChange={setShop}
    +             autoComplete="on"
    +             error={errors.shop}
    +           ></s-text-field>
    +           <s-button submit>Log in</s-button>
    +         </s-section>
    +       </Form>
    +     </s-page>
        </AppProvider>
      )
    }
  2. Add types for Polaris Web Components:

    .eslintrc.cjs:

    + rules: {
    +   "react/no-unknown-property": ["error", { ignore: ["variant"] }],
    + }   

    tsconfig.json:

    -   "types": ["@react-router/node", "vite/client"],
    +   "types": ["@react-router/node", "vite/client", "@shopify/app-bridge-ui-types"],

    package.json:

    "devDependencies": {
    +  "@shopify/app-bridge-ui-types": "^0.2.1",

With common changes done, you should upgrade your custom UI from Polaris React to Polaris Web components. Some resource to help:

We encourage you to migrate to Polaris web components. Doing so will save you maintenance overhead in the long run as you'll be on an evergreen component library.

If you prefer a gradual migration please see this guide on using both Polaris React and Polaris web components. Note that this is suboptimal as it mixes different design versions and forces users to download both Polaris React and Polaris Web Components.