-
Notifications
You must be signed in to change notification settings - Fork 21
Upgrading from Remix
- 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
AppProxyFormhave low adoption and won't be ported from Remix.
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.
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.
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.
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.
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:
-
Auth Code flow: This is our legacy mechanism. It supports non-embedded & embedded. It has an inferior merchant UX. It is enabled if the
unstable_newEmbeddedAuthStrategyfuture flag is not true -
Shopify managed install: This uses token exchange and is our default. It has a much superior merchant UX. It is enabled if the
unstable_newEmbeddedAuthStrategyfuture flag is true.
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.
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.
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-routerPlease see the CLI documentation for more information.
If you prefer to see all the changes we've prepared 2 migration branches:
- Most apps can use the RR App template migration branch
- Use the QR Code Example migration branch if you don't have the
vite.configfile.
Detailed instructions follow below. If you encounter issues please provide feedback.
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:
- Migrate your Remix app to Vite explains how to migrate a Shopify Remix app to Vite.
- This commit shows how to apply these changes.
- The official docs that describe this change.
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:
-
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" }, -
Update the tsconfig:
tsconfig.json- "types": ["node"] + "types": ["@remix-run/node", "vite/client"]
-
Adopt
v3_singleFetch:vite.config.tsremix({ 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); + };
-
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) withreturn datain every loader or action insideapp/routes/` -
(Optional) Update eslint config by replacing its content:
Here is the
.eslintrc.cjswe 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, }, }, ], };
-
(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 } }
These steps are described in the official upgrade from Remix guide.
-
Swap to React Router. First run the codemod:
npx codemod remix/2/react-router/upgrade && npm installMake sure
app/routes.tsis 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.
-
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"
-
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"] }
Now that React Router is set up it's time to swap @shopify/shopify-app-remix for @shopify/shopify-app-react-router.
-
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";
-
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.tsxlikeNavMenuitems.app/routes/auth.login/route.tsximport { - 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", },
You are ready to upgrade your UI to Polaris Web Components.
-
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> ) }
-
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:
- Polaris web components
- Upgrading the QR Code example app from Polaris React to Polaris Web Components.
- Upgrading the App template from Polaris React to Polaris Web Components.
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.