From 7109ff0327fe81661ef2ab780959ebb8f886eb60 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 13 Mar 2026 12:35:35 -0400 Subject: [PATCH 1/4] Fix missing forwardRef in AppProxyForm and AppProxyLink components AppProxyForm (shopify-app-remix), AppProxyLink (shopify-app-remix), and AppProxyLink (shopify-app-react-router) were plain function components, making it impossible to pass a ref to the underlying DOM element. Wrap all three with forwardRef so consumers can do things like form.current.submit() or anchor.current.focus(). Fixes #1605 Co-Authored-By: Claude Sonnet 4.6 --- .changeset/fix-appproxy-forwardref.md | 9 ++++++ .../components/AppProxyLink/AppProxyLink.tsx | 32 ++++++++++--------- .../components/AppProxyForm/AppProxyForm.tsx | 32 ++++++++++--------- .../components/AppProxyLink/AppProxyLink.tsx | 32 ++++++++++--------- 4 files changed, 60 insertions(+), 45 deletions(-) create mode 100644 .changeset/fix-appproxy-forwardref.md diff --git a/.changeset/fix-appproxy-forwardref.md b/.changeset/fix-appproxy-forwardref.md new file mode 100644 index 0000000000..c4e0b0e966 --- /dev/null +++ b/.changeset/fix-appproxy-forwardref.md @@ -0,0 +1,9 @@ +--- +'@shopify/shopify-app-remix': patch +'@shopify/shopify-app-react-router': patch +--- + +Forward refs in AppProxyForm and AppProxyLink components + +`AppProxyForm` and `AppProxyLink` now use `forwardRef`, allowing consumers to +attach a ref to the underlying DOM element (e.g. `form.current.submit()`). diff --git a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx index 3e743877c0..090fc47e26 100644 --- a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx +++ b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/AppProxyLink.tsx @@ -1,4 +1,4 @@ -import {useContext} from 'react'; +import {forwardRef, useContext} from 'react'; import {AppProxyProviderContext} from '../AppProxyProvider'; @@ -39,20 +39,22 @@ export interface AppProxyLinkProps extends React.DetailedHTMLProps< * } * ``` */ -export function AppProxyLink(props: AppProxyLinkProps) { - const context = useContext(AppProxyProviderContext); +export const AppProxyLink = forwardRef( + function AppProxyLink(props, ref) { + const context = useContext(AppProxyProviderContext); - if (!context) { - throw new Error( - 'AppProxyLink must be used within an AppProxyProvider component', - ); - } + if (!context) { + throw new Error( + 'AppProxyLink must be used within an AppProxyProvider component', + ); + } - const {children, href, ...otherProps} = props; + const {children, href, ...otherProps} = props; - return ( - - {children} - - ); -} + return ( + + {children} + + ); + }, +); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx index ca69c36ceb..8547d4279a 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/AppProxyForm.tsx @@ -1,4 +1,4 @@ -import {useContext} from 'react'; +import {forwardRef, useContext} from 'react'; import {Form, type FormProps} from '@remix-run/react'; import {AppProxyProviderContext} from '../AppProxyProvider'; @@ -63,20 +63,22 @@ export interface AppProxyFormProps extends FormProps { * } * ``` */ -export function AppProxyForm(props: AppProxyFormProps) { - const context = useContext(AppProxyProviderContext); +export const AppProxyForm = forwardRef( + function AppProxyForm(props, ref) { + const context = useContext(AppProxyProviderContext); - if (!context) { - throw new Error( - 'AppProxyForm must be used within an AppProxyProvider component', - ); - } + if (!context) { + throw new Error( + 'AppProxyForm must be used within an AppProxyProvider component', + ); + } - const {children, action, ...otherProps} = props; + const {children, action, ...otherProps} = props; - return ( -
- {children} -
- ); -} + return ( +
+ {children} +
+ ); + }, +); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx index 76b7403529..c7c2e3f351 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/AppProxyLink.tsx @@ -1,4 +1,4 @@ -import {useContext} from 'react'; +import {forwardRef, useContext} from 'react'; import {AppProxyProviderContext} from '../AppProxyProvider'; @@ -39,20 +39,22 @@ export interface AppProxyLinkProps extends React.DetailedHTMLProps< * } * ``` */ -export function AppProxyLink(props: AppProxyLinkProps) { - const context = useContext(AppProxyProviderContext); +export const AppProxyLink = forwardRef( + function AppProxyLink(props, ref) { + const context = useContext(AppProxyProviderContext); - if (!context) { - throw new Error( - 'AppProxyLink must be used within an AppProxyProvider component', - ); - } + if (!context) { + throw new Error( + 'AppProxyLink must be used within an AppProxyProvider component', + ); + } - const {children, href, ...otherProps} = props; + const {children, href, ...otherProps} = props; - return ( - - {children} - - ); -} + return ( + + {children} + + ); + }, +); From b3a1761835060f477ba557351b7baf9ee1e5a347 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 13 Mar 2026 13:20:20 -0400 Subject: [PATCH 2/4] Add ref forwarding tests and split changesets per package - Add forwardRef test to AppProxyForm (shopify-app-remix) - Add forwardRef tests to AppProxyLink in both shopify-app-remix and shopify-app-react-router - Replace combined changeset with per-package changesets listing each affected component Co-Authored-By: Claude Sonnet 4.6 --- .../fix-appproxy-react-router-forwardref.md | 6 ++++++ ...ef.md => fix-appproxy-remix-forwardref.md} | 3 --- .../__tests__/AppProxyLink.test.tsx | 16 ++++++++++++++ .../__tests__/AppProxyForm.test.tsx | 21 +++++++++++++++++++ .../__tests__/AppProxyLink.test.tsx | 16 ++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-appproxy-react-router-forwardref.md rename .changeset/{fix-appproxy-forwardref.md => fix-appproxy-remix-forwardref.md} (66%) diff --git a/.changeset/fix-appproxy-react-router-forwardref.md b/.changeset/fix-appproxy-react-router-forwardref.md new file mode 100644 index 0000000000..eff52380fb --- /dev/null +++ b/.changeset/fix-appproxy-react-router-forwardref.md @@ -0,0 +1,6 @@ +--- +'@shopify/shopify-app-react-router': patch +--- + +`AppProxyLink` now uses `forwardRef`, allowing consumers to attach a ref to the +underlying `` element (e.g. `anchor.current.focus()`). diff --git a/.changeset/fix-appproxy-forwardref.md b/.changeset/fix-appproxy-remix-forwardref.md similarity index 66% rename from .changeset/fix-appproxy-forwardref.md rename to .changeset/fix-appproxy-remix-forwardref.md index c4e0b0e966..50db94184b 100644 --- a/.changeset/fix-appproxy-forwardref.md +++ b/.changeset/fix-appproxy-remix-forwardref.md @@ -1,9 +1,6 @@ --- '@shopify/shopify-app-remix': patch -'@shopify/shopify-app-react-router': patch --- -Forward refs in AppProxyForm and AppProxyLink components - `AppProxyForm` and `AppProxyLink` now use `forwardRef`, allowing consumers to attach a ref to the underlying DOM element (e.g. `form.current.submit()`). diff --git a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx index dbb47271b0..e009be3cf5 100644 --- a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx +++ b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx @@ -1,3 +1,4 @@ +import {createRef} from 'react'; import {mount} from '@shopify/react-testing'; import '../../../__tests__/test-helper'; @@ -45,4 +46,19 @@ describe('', () => { }); expect(component).toContainReactHtml('Hello world'); }); + + it('forwards ref to the underlying anchor element', () => { + // GIVEN + const ref = createRef(); + + // WHEN + mount( + + Hello world + , + ); + + // THEN + expect(ref.current).toBeInstanceOf(HTMLAnchorElement); + }); }); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx index 7df0149c28..9c3f42d9ab 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx @@ -1,3 +1,4 @@ +import {createRef} from 'react'; import {mount} from '@shopify/react-testing'; import {createRemixStub} from '@remix-run/testing'; @@ -52,4 +53,24 @@ describe('', () => { expect(component).toContainReactComponent('form', {action: '/my-action/'}); expect(component).toContainReactHtml('Hello world'); }); + + it('forwards ref to the underlying form element', () => { + // GIVEN + const ref = createRef(); + + function MyComponent() { + return ( + + Hello world + + ); + } + + // WHEN + const RemixStub = createRemixStub([{path: '/', Component: MyComponent}]); + mount(); + + // THEN + expect(ref.current).toBeInstanceOf(HTMLFormElement); + }); }); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx index dbb47271b0..e009be3cf5 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx @@ -1,3 +1,4 @@ +import {createRef} from 'react'; import {mount} from '@shopify/react-testing'; import '../../../__tests__/test-helper'; @@ -45,4 +46,19 @@ describe('', () => { }); expect(component).toContainReactHtml('Hello world'); }); + + it('forwards ref to the underlying anchor element', () => { + // GIVEN + const ref = createRef(); + + // WHEN + mount( + + Hello world + , + ); + + // THEN + expect(ref.current).toBeInstanceOf(HTMLAnchorElement); + }); }); From fb21556d4bfa56c2b484e25706970c7182919c78 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 13 Mar 2026 13:22:37 -0400 Subject: [PATCH 3/4] Fix prettier formatting in AppProxyLink ref tests Co-Authored-By: Claude Sonnet 4.6 --- .../components/AppProxyLink/__tests__/AppProxyLink.test.tsx | 4 +++- .../components/AppProxyLink/__tests__/AppProxyLink.test.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx index e009be3cf5..61440758aa 100644 --- a/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx +++ b/packages/apps/shopify-app-react-router/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx @@ -54,7 +54,9 @@ describe('', () => { // WHEN mount( - Hello world + + Hello world + , ); diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx index e009be3cf5..61440758aa 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyLink/__tests__/AppProxyLink.test.tsx @@ -54,7 +54,9 @@ describe('', () => { // WHEN mount( - Hello world + + Hello world + , ); From 9a2706e78b3cf86e64ede1bab249da1a92720b19 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Fri, 13 Mar 2026 13:28:17 -0400 Subject: [PATCH 4/4] Fix prettier formatting in AppProxyForm ref test Co-Authored-By: Claude Sonnet 4.6 --- .../components/AppProxyForm/__tests__/AppProxyForm.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx index 9c3f42d9ab..d28b878b0a 100644 --- a/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx +++ b/packages/apps/shopify-app-remix/src/react/components/AppProxyForm/__tests__/AppProxyForm.test.tsx @@ -61,7 +61,9 @@ describe('', () => { function MyComponent() { return ( - Hello world + + Hello world + ); }