From 8fd15b3642513f3da94bc15c55338a32f73ea1a9 Mon Sep 17 00:00:00 2001 From: Benjamin Thomas Date: Mon, 8 Jun 2026 11:59:35 +0200 Subject: [PATCH 1/4] Fix useRefetchable/fromData incompatibility --- .../__tests__/Test_refetching-tests.js | 7 ++- .../__tests__/Test_refetching.res | 28 +++++++++++ .../src/RescriptRelay_Fragment.res | 48 ++++++++++++------- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/packages/rescript-relay/__tests__/Test_refetching-tests.js b/packages/rescript-relay/__tests__/Test_refetching-tests.js index b3a00d83..9ebda74a 100644 --- a/packages/rescript-relay/__tests__/Test_refetching-tests.js +++ b/packages/rescript-relay/__tests__/Test_refetching-tests.js @@ -4,7 +4,7 @@ const React = require("react"); const queryMock = require("./queryMock"); const ReactTestUtils = require("react-dom/test-utils"); -const { test_refetching } = require("./Test_refetching.bs"); +const { test_refetching, test_refetchable_component } = require("./Test_refetching.bs"); describe("Fragment", () => { test("refetching works", async () => { @@ -70,4 +70,9 @@ describe("Fragment", () => { await t.screen.findByText("First is online"); await t.screen.findByText("Friends: 10"); }); + + test("useRefetchable supports `fromData`", async () => { + t.render(test_refetchable_component()); + await t.screen.findByText("Test Data has 3 friends"); + }); }); diff --git a/packages/rescript-relay/__tests__/Test_refetching.res b/packages/rescript-relay/__tests__/Test_refetching.res index d8da9d67..b97915e7 100644 --- a/packages/rescript-relay/__tests__/Test_refetching.res +++ b/packages/rescript-relay/__tests__/Test_refetching.res @@ -83,6 +83,18 @@ module Test = { } } +module RefetchableComponent = { + @react.component + let make = (~user) => { + let (data, _refetch) = Fragment.useRefetchable(user) +
+ {React.string( + `${data.firstName} has ${data.friendsConnection.totalCount->Int.toString} friends`, + )} +
+ } +} + @live let test_refetching = () => { let network = RescriptRelay.Network.makePromiseBased(~fetchFunction=RelayEnv.fetchQuery) @@ -96,3 +108,19 @@ let test_refetching = () => { } + +@live +let test_refetchable_component = () => { + let environment = RescriptRelay_Test.createMockEnvironment() + + + +} diff --git a/packages/rescript-relay/src/RescriptRelay_Fragment.res b/packages/rescript-relay/src/RescriptRelay_Fragment.res index a08743d6..35683383 100644 --- a/packages/rescript-relay/src/RescriptRelay_Fragment.res +++ b/packages/rescript-relay/src/RescriptRelay_Fragment.res @@ -250,6 +250,18 @@ external useRefetchableFragment_: ( 'fragmentRef, ) => ('fragment, ('refetchVariables, refetchableFnOpts) => Disposable.t) = "useRefetchableFragment" +@obj external internal_makeDisposable: (~dispose: unit => unit) => Disposable.t = "" + +let internal_noopDisposable: Disposable.t = internal_makeDisposable(~dispose=() => ()) + +let internal_noopRefetch = (~variables as _, ~fetchPolicy as _=?, ~onComplete=?) => { + switch onComplete { + | None => () + | Some(complete) => complete(None) + } + internal_noopDisposable +} + /**React hook for using a fragment that you want to refetch. Returns \ a tuple of `(fragmentData, refetchFn)`.\n\n\ ### Refetching and variables\n\ @@ -267,22 +279,26 @@ let useRefetchableFragment = ( ~convertRefetchVariables: 'refetchVariables => 'refetchVariables, ~fRef, ) => { - let (fragmentData, refetchFn) = useRefetchableFragment_(node, fRef) - let data = RescriptRelay_Internal.internal_useConvertedValue(convertFragment, fragmentData) - ( - data, - React.useMemo1( - () => - (~variables: 'refetchVariables, ~fetchPolicy=?, ~onComplete=?) => - refetchFn( - RescriptRelay_Internal.internal_removeUndefinedAndConvertNullsRaw( - variables->convertRefetchVariables, - )->RescriptRelay_Internal.internal_cleanObjectFromUndefinedRaw, - internal_makeRefetchableFnOpts(~fetchPolicy?, ~onComplete?, ()), - ), - [refetchFn], - ), - ) + switch RescriptRelay_TestFragmentRef.getDataForNode(node, fRef) { + | Some(data) => (data, internal_noopRefetch) + | None => + let (fragmentData, refetchFn) = useRefetchableFragment_(node, fRef) + let data = RescriptRelay_Internal.internal_useConvertedValue(convertFragment, fragmentData) + ( + data, + React.useMemo1( + () => + (~variables: 'refetchVariables, ~fetchPolicy=?, ~onComplete=?) => + refetchFn( + RescriptRelay_Internal.internal_removeUndefinedAndConvertNullsRaw( + variables->convertRefetchVariables, + )->RescriptRelay_Internal.internal_cleanObjectFromUndefinedRaw, + internal_makeRefetchableFnOpts(~fetchPolicy?, ~onComplete?, ()), + ), + [refetchFn], + ), + ) + } } @module("relay-runtime/experimental") From 9868f93e534cae296abb91279b476d411b280638 Mon Sep 17 00:00:00 2001 From: Benjamin Thomas Date: Wed, 10 Jun 2026 19:50:45 +0200 Subject: [PATCH 2/4] Make the disposable noop cleaner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Paul Tsnobiladzé --- .../src/RescriptRelay_Fragment.res | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/rescript-relay/src/RescriptRelay_Fragment.res b/packages/rescript-relay/src/RescriptRelay_Fragment.res index 35683383..466e7182 100644 --- a/packages/rescript-relay/src/RescriptRelay_Fragment.res +++ b/packages/rescript-relay/src/RescriptRelay_Fragment.res @@ -250,16 +250,21 @@ external useRefetchableFragment_: ( 'fragmentRef, ) => ('fragment, ('refetchVariables, refetchableFnOpts) => Disposable.t) = "useRefetchableFragment" -@obj external internal_makeDisposable: (~dispose: unit => unit) => Disposable.t = "" - -let internal_noopDisposable: Disposable.t = internal_makeDisposable(~dispose=() => ()) - -let internal_noopRefetch = (~variables as _, ~fetchPolicy as _=?, ~onComplete=?) => { +let internal_noopRefetch = ( + ~variables as _, + ~fetchPolicy as _=?, + ~onComplete=?, +) => { + module Disposable = { + type t = {dispose: unit => unit} + external toOpaqueDisposable: t => Disposable.t = "%identity" + let noop = { dispose : () => ()}->toOpaqueDisposable + } switch onComplete { | None => () | Some(complete) => complete(None) } - internal_noopDisposable + Disposable.noop } /**React hook for using a fragment that you want to refetch. Returns \ From d9edfe58f674ed0b96871d6c7adac2f900be4fde Mon Sep 17 00:00:00 2001 From: Benjamin Thomas Date: Wed, 10 Jun 2026 19:58:06 +0200 Subject: [PATCH 3/4] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e651bccf..5f04fb9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # master - fix remove-unused-fields command. https://github.com/zth/rescript-relay/pull/636 +- Fix `useRefetchableFragment` to be compatible with `fromData`. # 4.5.1 From bb5d095943d5392d519ba32ebcae620da24150cb Mon Sep 17 00:00:00 2001 From: Benjamin Thomas Date: Tue, 23 Jun 2026 18:27:46 +0200 Subject: [PATCH 4/4] Fix usePaginationFragment/fromData incompatibility --- CHANGELOG.md | 2 +- .../__tests__/Test_paginationInNode-tests.js | 7 +- .../__tests__/Test_paginationInNode.res | 45 +++++++ .../src/RescriptRelay_Fragment.res | 125 +++++++++++------- 4 files changed, 126 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f04fb9b..f98b16df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # master - fix remove-unused-fields command. https://github.com/zth/rescript-relay/pull/636 -- Fix `useRefetchableFragment` to be compatible with `fromData`. +- Fix `useRefetchableFragment` and `usePaginationFragment` to be compatible with `fromData`. # 4.5.1 diff --git a/packages/rescript-relay/__tests__/Test_paginationInNode-tests.js b/packages/rescript-relay/__tests__/Test_paginationInNode-tests.js index 4ac5e83c..6d4be552 100644 --- a/packages/rescript-relay/__tests__/Test_paginationInNode-tests.js +++ b/packages/rescript-relay/__tests__/Test_paginationInNode-tests.js @@ -4,7 +4,7 @@ const ReactTestUtils = require("react-dom/test-utils"); const React = require("react"); const queryMock = require("./queryMock"); -const { test_pagination } = require("./Test_paginationInNode.bs"); +const { test_pagination, test_pagination_from_data } = require("./Test_paginationInNode.bs"); describe("Pagination nested in node", () => { test("paginating works", async () => { @@ -180,4 +180,9 @@ describe("Pagination nested in node", () => { await t.screen.findByText("User Second has 3 friends"); expect(t.screen.queryByText("User First has 2 friends")).toBeFalsy(); }); + + test("usePagination supports `fromData`", async () => { + t.render(test_pagination_from_data()); + await t.screen.findByText("Test Data has 2 friends"); + }); }); diff --git a/packages/rescript-relay/__tests__/Test_paginationInNode.res b/packages/rescript-relay/__tests__/Test_paginationInNode.res index 6a469928..00188db2 100644 --- a/packages/rescript-relay/__tests__/Test_paginationInNode.res +++ b/packages/rescript-relay/__tests__/Test_paginationInNode.res @@ -113,6 +113,15 @@ module Test = { } } +module FromDataPaginationComponent = { + @react.component + let make = (~user) => { + let {data} = Fragment.usePagination(user) + let friendCount = data.friendsConnection->Fragment.getConnectionNodes->Array.length +
{React.string(`Test Data has ${friendCount->Int.toString} friends`)}
+ } +} + @live let test_pagination = () => { let network = RescriptRelay.Network.makePromiseBased(~fetchFunction=RelayEnv.fetchQuery) @@ -126,3 +135,39 @@ let test_pagination = () => { } + +@live +let test_pagination_from_data = () => { + let environment = RescriptRelay_Test.createMockEnvironment() + + + +} diff --git a/packages/rescript-relay/src/RescriptRelay_Fragment.res b/packages/rescript-relay/src/RescriptRelay_Fragment.res index 466e7182..142337e9 100644 --- a/packages/rescript-relay/src/RescriptRelay_Fragment.res +++ b/packages/rescript-relay/src/RescriptRelay_Fragment.res @@ -87,8 +87,35 @@ let internal_makeRefetchableFnOpts = (~fetchPolicy=?, ~onComplete=?, ()) => { onComplete: ?(onComplete->RescriptRelay_Internal.internal_nullableToOptionalExnHandler), } +module InternalNoopDisposable = { + type t = {dispose: unit => unit} + external toOpaqueDisposable: t => Disposable.t = "%identity" + let noop = {dispose: () => ()}->toOpaqueDisposable +} + +let internal_noopRefetch = ( + ~variables as _, + ~fetchPolicy as _=?, + ~onComplete=?, +) => { + switch onComplete { + | None => () + | Some(complete) => complete(None) + } + InternalNoopDisposable.noop +} + type paginationLoadMoreOptions = {onComplete?: Nullable.t => unit} type paginationLoadMoreFn = (~count: int, ~onComplete: option => unit=?) => Disposable.t + +let internal_noopLoadMore = (~count as _, ~onComplete=?) => { + switch onComplete { + | None => () + | Some(complete) => complete(None) + } + InternalNoopDisposable.noop +} + type paginationFragmentReturnRaw<'fragment, 'refetchVariables> = { data: 'fragment, loadNext: (int, paginationLoadMoreOptions) => Disposable.t, @@ -127,40 +154,53 @@ let usePaginationFragment = ( ~convertFragment: 'fragment => 'fragment, ~convertRefetchVariables: 'refetchVariables => 'refetchVariables, ) => { - let p = usePaginationFragment_(node, fRef) - let data = RescriptRelay_Internal.internal_useConvertedValue(convertFragment, p.data) - { - data, - loadNext: React.useMemo1(() => - (~count, ~onComplete=?) => { - p.loadNext( - count, - {onComplete: ?(onComplete->RescriptRelay_Internal.internal_nullableToOptionalExnHandler)}, - ) - } - , [p.loadNext]), - loadPrevious: React.useMemo1(() => - (~count, ~onComplete=?) => { - p.loadPrevious( - count, - {onComplete: ?(onComplete->RescriptRelay_Internal.internal_nullableToOptionalExnHandler)}, - ) - } - , [p.loadPrevious]), - hasNext: p.hasNext, - hasPrevious: p.hasPrevious, - isLoadingNext: p.isLoadingNext, - isLoadingPrevious: p.isLoadingPrevious, - refetch: React.useMemo1(() => - (~variables, ~fetchPolicy=?, ~onComplete=?) => { - p.refetch( - RescriptRelay_Internal.internal_cleanObjectFromUndefinedRaw( - variables->convertRefetchVariables, - ), - internal_makeRefetchableFnOpts(~onComplete?, ~fetchPolicy?, ()), - ) - } - , [p.refetch]), + switch RescriptRelay_TestFragmentRef.getDataForNode(node, fRef) { + | Some(data) => { + data, + loadNext: internal_noopLoadMore, + loadPrevious: internal_noopLoadMore, + hasNext: false, + hasPrevious: false, + isLoadingNext: false, + isLoadingPrevious: false, + refetch: internal_noopRefetch, + } + | None => + let p = usePaginationFragment_(node, fRef) + let data = RescriptRelay_Internal.internal_useConvertedValue(convertFragment, p.data) + { + data, + loadNext: React.useMemo1(() => + (~count, ~onComplete=?) => { + p.loadNext( + count, + {onComplete: ?(onComplete->RescriptRelay_Internal.internal_nullableToOptionalExnHandler)}, + ) + } + , [p.loadNext]), + loadPrevious: React.useMemo1(() => + (~count, ~onComplete=?) => { + p.loadPrevious( + count, + {onComplete: ?(onComplete->RescriptRelay_Internal.internal_nullableToOptionalExnHandler)}, + ) + } + , [p.loadPrevious]), + hasNext: p.hasNext, + hasPrevious: p.hasPrevious, + isLoadingNext: p.isLoadingNext, + isLoadingPrevious: p.isLoadingPrevious, + refetch: React.useMemo1(() => + (~variables, ~fetchPolicy=?, ~onComplete=?) => { + p.refetch( + RescriptRelay_Internal.internal_cleanObjectFromUndefinedRaw( + variables->convertRefetchVariables, + ), + internal_makeRefetchableFnOpts(~onComplete?, ~fetchPolicy?, ()), + ) + } + , [p.refetch]), + } } } @@ -250,23 +290,6 @@ external useRefetchableFragment_: ( 'fragmentRef, ) => ('fragment, ('refetchVariables, refetchableFnOpts) => Disposable.t) = "useRefetchableFragment" -let internal_noopRefetch = ( - ~variables as _, - ~fetchPolicy as _=?, - ~onComplete=?, -) => { - module Disposable = { - type t = {dispose: unit => unit} - external toOpaqueDisposable: t => Disposable.t = "%identity" - let noop = { dispose : () => ()}->toOpaqueDisposable - } - switch onComplete { - | None => () - | Some(complete) => complete(None) - } - Disposable.noop -} - /**React hook for using a fragment that you want to refetch. Returns \ a tuple of `(fragmentData, refetchFn)`.\n\n\ ### Refetching and variables\n\