diff --git a/packages/react-relay/__tests__/RelayResolvers-withOutputType-test.js b/packages/react-relay/__tests__/RelayResolvers-withOutputType-test.js
index 6230ad2ae370b..59726d13be65d 100644
--- a/packages/react-relay/__tests__/RelayResolvers-withOutputType-test.js
+++ b/packages/react-relay/__tests__/RelayResolvers-withOutputType-test.js
@@ -279,6 +279,26 @@ describe.each([
});
}
+ function ManyLiveTodosComponent() {
+ const data = useClientQuery(
+ graphql`
+ query RelayResolversWithOutputTypeTestManyLiveTodosQuery {
+ many_live_todos {
+ ...RelayResolversWithOutputTypeTestFragment
+ }
+ }
+ `,
+ {},
+ );
+ if (data.many_live_todos?.length === 0) {
+ return 'No Items';
+ }
+
+ return data.many_live_todos?.map((todo, index) => {
+ return ;
+ });
+ }
+
test('should render empty state', () => {
const renderer = TestRenderer.create(
@@ -718,4 +738,44 @@ describe.each([
'color: color is red',
]);
});
+
+ test('rendering live list', () => {
+ addTodo('Todo 1');
+ addTodo('Todo 2');
+ addTodo('Todo 3');
+
+ const renderer = TestRenderer.create(
+
+
+ ,
+ );
+
+ expect(renderer.toJSON()).toEqual([
+ 'Todo 1',
+ 'is not completed',
+ 'style: bold',
+ 'color: color is red',
+ 'Todo 2',
+ 'is not completed',
+ 'style: bold',
+ 'color: color is red',
+ 'Todo 3',
+ 'is not completed',
+ 'style: bold',
+ 'color: color is red',
+ ]);
+
+ TestRenderer.act(() => {
+ removeTodo('todo-1');
+ removeTodo('todo-2');
+ jest.runAllImmediates();
+ });
+
+ expect(renderer.toJSON()).toEqual([
+ 'Todo 3',
+ 'is not completed',
+ 'style: bold',
+ 'color: color is red',
+ ]);
+ });
});
diff --git a/packages/react-relay/__tests__/__generated__/RelayResolversWithOutputTypeTestManyLiveTodosQuery.graphql.js b/packages/react-relay/__tests__/__generated__/RelayResolversWithOutputTypeTestManyLiveTodosQuery.graphql.js
new file mode 100644
index 0000000000000..b66de38224b06
--- /dev/null
+++ b/packages/react-relay/__tests__/__generated__/RelayResolversWithOutputTypeTestManyLiveTodosQuery.graphql.js
@@ -0,0 +1,251 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @oncall relay
+ *
+ * @generated SignedSource<<451e85f22d8b3a215a63a985c1fd88e7>>
+ * @flow
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { ClientRequest, ClientQuery } from 'relay-runtime';
+import type { LiveState } from "relay-runtime/store/experimental-live-resolvers/LiveResolverStore";
+import type { RelayResolversWithOutputTypeTestFragment$fragmentType } from "./RelayResolversWithOutputTypeTestFragment.graphql";
+import {many_live_todos as queryManyLiveTodosResolverType} from "../../../relay-runtime/store/__tests__/resolvers/QueryManyLiveTodos.js";
+// Type assertion validating that `queryManyLiveTodosResolverType` resolver is correctly implemented.
+// A type error here indicates that the type signature of the resolver module is incorrect.
+(queryManyLiveTodosResolverType: () => LiveState<$ReadOnlyArray>);
+import type { Query__many_live_todos$normalization } from "./../../../relay-runtime/store/__tests__/resolvers/__generated__/Query__many_live_todos$normalization.graphql";
+export type RelayResolversWithOutputTypeTestManyLiveTodosQuery$variables = {||};
+export type RelayResolversWithOutputTypeTestManyLiveTodosQuery$data = {|
+ +many_live_todos: ?$ReadOnlyArray{|
+ +$fragmentSpreads: RelayResolversWithOutputTypeTestFragment$fragmentType,
+ |}>,
+|};
+export type RelayResolversWithOutputTypeTestManyLiveTodosQuery = {|
+ response: RelayResolversWithOutputTypeTestManyLiveTodosQuery$data,
+ variables: RelayResolversWithOutputTypeTestManyLiveTodosQuery$variables,
+|};
+*/
+
+var node/*: ClientRequest*/ = (function(){
+var v0 = {
+ "kind": "InlineFragment",
+ "selections": [
+ {
+ "name": "self",
+ "args": null,
+ "fragment": {
+ "kind": "InlineFragment",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "todo_id",
+ "storageKey": null
+ }
+ ],
+ "type": "Todo",
+ "abstractKey": null
+ },
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": false
+ }
+ ],
+ "type": "Todo",
+ "abstractKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": {
+ "hasClientEdges": true
+ },
+ "name": "RelayResolversWithOutputTypeTestManyLiveTodosQuery",
+ "selections": [
+ {
+ "kind": "ClientEdgeToClientObject",
+ "concreteType": "Todo",
+ "backingField": {
+ "alias": null,
+ "args": null,
+ "fragment": null,
+ "kind": "RelayLiveResolver",
+ "name": "many_live_todos",
+ "resolverModule": require('./../../../relay-runtime/store/__tests__/resolvers/QueryManyLiveTodos').many_live_todos,
+ "path": "many_live_todos",
+ "normalizationInfo": {
+ "concreteType": "Todo",
+ "plural": true,
+ "normalizationNode": require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/Query__many_live_todos$normalization.graphql')
+ }
+ },
+ "linkedField": {
+ "alias": null,
+ "args": null,
+ "concreteType": "Todo",
+ "kind": "LinkedField",
+ "name": "many_live_todos",
+ "plural": true,
+ "selections": [
+ {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "RelayResolversWithOutputTypeTestFragment"
+ }
+ ],
+ "storageKey": null
+ }
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "RelayResolversWithOutputTypeTestManyLiveTodosQuery",
+ "selections": [
+ {
+ "kind": "ClientEdgeToClientObject",
+ "backingField": {
+ "name": "many_live_todos",
+ "args": null,
+ "fragment": null,
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": true
+ },
+ "linkedField": {
+ "alias": null,
+ "args": null,
+ "concreteType": "Todo",
+ "kind": "LinkedField",
+ "name": "many_live_todos",
+ "plural": true,
+ "selections": [
+ {
+ "kind": "ClientEdgeToClientObject",
+ "backingField": {
+ "name": "text",
+ "args": null,
+ "fragment": (v0/*: any*/),
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": true
+ },
+ "linkedField": {
+ "alias": null,
+ "args": null,
+ "concreteType": "TodoText",
+ "kind": "LinkedField",
+ "name": "text",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "content",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "TodoTextStyle",
+ "kind": "LinkedField",
+ "name": "style",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "font_style",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "TodoTextColor",
+ "kind": "LinkedField",
+ "name": "color",
+ "plural": false,
+ "selections": [
+ {
+ "name": "human_readable_color",
+ "args": null,
+ "fragment": {
+ "kind": "InlineFragment",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "hex",
+ "storageKey": null
+ }
+ ],
+ "type": "TodoTextColor",
+ "abstractKey": null
+ },
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": false
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ },
+ {
+ "name": "complete",
+ "args": null,
+ "fragment": (v0/*: any*/),
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": false
+ }
+ ],
+ "storageKey": null
+ }
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "b9aabe2c5911f0a5daf91a73d666b31c",
+ "id": null,
+ "metadata": {},
+ "name": "RelayResolversWithOutputTypeTestManyLiveTodosQuery",
+ "operationKind": "query",
+ "text": null
+ }
+};
+})();
+
+if (__DEV__) {
+ (node/*: any*/).hash = "f42ffca5f81738e839984490939acc31";
+}
+
+module.exports = ((node/*: any*/)/*: ClientQuery<
+ RelayResolversWithOutputTypeTestManyLiveTodosQuery$variables,
+ RelayResolversWithOutputTypeTestManyLiveTodosQuery$data,
+>*/);
diff --git a/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js b/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js
index 691dd61b1cfb7..3893687cacfea 100644
--- a/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js
+++ b/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js
@@ -13,7 +13,7 @@
import type {LogEvent} from '../../RelayStoreTypes';
-export opaque type TodoID = string;
+export opaque type TodoID: string = string;
export type TodoItem = {
todoID: TodoID,
diff --git a/packages/relay-runtime/store/__tests__/resolvers/QueryManyLiveTodos.js b/packages/relay-runtime/store/__tests__/resolvers/QueryManyLiveTodos.js
new file mode 100644
index 0000000000000..4c4f1c4bfd037
--- /dev/null
+++ b/packages/relay-runtime/store/__tests__/resolvers/QueryManyLiveTodos.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow strict-local
+ * @format
+ * @oncall relay
+ */
+
+'use strict';
+
+import type {LiveState} from '../../experimental-live-resolvers/LiveResolverStore';
+
+const {
+ Selectors,
+ TODO_STORE,
+} = require('relay-runtime/store/__tests__/resolvers/ExampleTodoStore');
+
+/**
+ * @RelayResolver Query.many_live_todos: [Todo]
+ * @live
+ */
+function many_live_todos(): LiveState<$ReadOnlyArray<{todo_id: string}>> {
+ return {
+ read() {
+ return Selectors.getTodoIDs(TODO_STORE.getState()).map(id => ({
+ todo_id: id,
+ }));
+ },
+ subscribe(cb) {
+ return TODO_STORE.subscribe(null, cb);
+ },
+ };
+}
+
+module.exports = {
+ many_live_todos,
+};
diff --git a/packages/relay-runtime/store/__tests__/resolvers/__generated__/Query__many_live_todos$normalization.graphql.js b/packages/relay-runtime/store/__tests__/resolvers/__generated__/Query__many_live_todos$normalization.graphql.js
new file mode 100644
index 0000000000000..42b692cb0eec8
--- /dev/null
+++ b/packages/relay-runtime/store/__tests__/resolvers/__generated__/Query__many_live_todos$normalization.graphql.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @oncall relay
+ *
+ * @generated SignedSource<<74fd0ae7e5c35857bcd216ec356d5ceb>>
+ * @flow
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { NormalizationSplitOperation } from 'relay-runtime';
+
+export type Query__many_live_todos$normalization = {|
+ +todo_id: string,
+|};
+
+*/
+
+var node/*: NormalizationSplitOperation*/ = {
+ "kind": "SplitOperation",
+ "metadata": {},
+ "name": "Query__many_live_todos$normalization",
+ "selections": [
+ {
+ "kind": "ClientExtension",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "todo_id",
+ "storageKey": null
+ }
+ ]
+ }
+ ]
+};
+
+module.exports = node;