diff --git a/todo/__generated__/queries.json b/todo/__generated__/queries.json index 36a1f805..42965194 100644 --- a/todo/__generated__/queries.json +++ b/todo/__generated__/queries.json @@ -1,8 +1,8 @@ { "4d3a6acb198829c5ed5f748574934211": "mutation RemoveCompletedTodosMutation(\n $input: RemoveCompletedTodosInput!\n) {\n removeCompletedTodos(input: $input) {\n deletedTodoIds\n user {\n id\n completedCount\n totalCount\n }\n }\n}\n", - "8a75762b2e92a08aeb1007fc11f71298": "query TodoAppQuery(\n $userId: String\n) {\n user(id: $userId) {\n ...TodoList_user\n id\n }\n}\n\nfragment AddTodoMutation_user on User {\n userId\n id\n totalCount\n}\n\nfragment ChangeTodoStatusMutation_todo on Todo {\n id\n}\n\nfragment ChangeTodoStatusMutation_user on User {\n id\n userId\n completedCount\n}\n\nfragment MarkAllTodosMutation_todoEdge on TodoEdge {\n node {\n id\n }\n}\n\nfragment MarkAllTodosMutation_user on User {\n id\n userId\n totalCount\n}\n\nfragment RemoveCompletedTodosMutation_todoConnection on TodoConnection {\n edges {\n node {\n id\n complete\n }\n }\n}\n\nfragment RemoveCompletedTodosMutation_user on User {\n id\n userId\n totalCount\n}\n\nfragment RemoveTodoMutation_todo on Todo {\n id\n complete\n}\n\nfragment RemoveTodoMutation_user on User {\n id\n userId\n totalCount\n completedCount\n}\n\nfragment RenameTodoMutation_todo on Todo {\n id\n text\n}\n\nfragment TodoListFooter_todoConnection on TodoConnection {\n ...RemoveCompletedTodosMutation_todoConnection\n}\n\nfragment TodoListFooter_user on User {\n totalCount\n completedCount\n ...RemoveCompletedTodosMutation_user\n}\n\nfragment TodoList_user on User {\n todos(first: 2147483647) {\n edges {\n node {\n id\n ...Todo_todo\n __typename\n }\n ...MarkAllTodosMutation_todoEdge\n cursor\n }\n ...TodoListFooter_todoConnection\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n totalCount\n completedCount\n ...AddTodoMutation_user\n ...MarkAllTodosMutation_user\n ...Todo_user\n ...TodoListFooter_user\n}\n\nfragment Todo_todo on Todo {\n complete\n text\n ...ChangeTodoStatusMutation_todo\n ...RenameTodoMutation_todo\n ...RemoveTodoMutation_todo\n}\n\nfragment Todo_user on User {\n ...ChangeTodoStatusMutation_user\n ...RemoveTodoMutation_user\n}\n", + "771fc5cec159852d7952e9a29ae605b4": "mutation AddTodoMutation(\n $input: AddTodoInput!\n) {\n addTodo(input: $input) {\n todoEdge {\n node {\n id\n text\n }\n }\n user {\n id\n totalCount\n }\n }\n}\n", + "85b55396fb97187f9eeb6b8511ad5de7": "query TodoAppQuery(\n $userId: String\n) {\n user(id: $userId) {\n todos(first: 2147483647) {\n edges {\n node {\n id\n __typename\n }\n cursor\n }\n ...TodoList_todos\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n ...TodoList_user\n id\n }\n}\n\nfragment AddTodoMutation_user on User {\n userId\n id\n totalCount\n}\n\nfragment ChangeTodoStatusMutation_todo on Todo {\n id\n}\n\nfragment ChangeTodoStatusMutation_user on User {\n id\n userId\n completedCount\n}\n\nfragment MarkAllTodosMutation_todoEdge on TodoEdge {\n node {\n id\n }\n}\n\nfragment MarkAllTodosMutation_user on User {\n id\n userId\n totalCount\n}\n\nfragment RemoveCompletedTodosMutation_todoConnection on TodoConnection {\n edges {\n node {\n id\n complete\n }\n }\n}\n\nfragment RemoveCompletedTodosMutation_user on User {\n id\n userId\n totalCount\n}\n\nfragment RemoveTodoMutation_todo on Todo {\n id\n complete\n}\n\nfragment RemoveTodoMutation_user on User {\n id\n userId\n totalCount\n completedCount\n}\n\nfragment RenameTodoMutation_todo on Todo {\n id\n text\n}\n\nfragment TodoListFooter_todoConnection on TodoConnection {\n ...RemoveCompletedTodosMutation_todoConnection\n}\n\nfragment TodoListFooter_user on User {\n totalCount\n completedCount\n ...RemoveCompletedTodosMutation_user\n}\n\nfragment TodoList_todos on TodoConnection {\n edges {\n node {\n id\n ...Todo_todo\n }\n ...MarkAllTodosMutation_todoEdge\n }\n ...TodoListFooter_todoConnection\n}\n\nfragment TodoList_user on User {\n totalCount\n completedCount\n ...AddTodoMutation_user\n ...MarkAllTodosMutation_user\n ...Todo_user\n ...TodoListFooter_user\n}\n\nfragment Todo_todo on Todo {\n complete\n text\n ...ChangeTodoStatusMutation_todo\n ...RenameTodoMutation_todo\n ...RemoveTodoMutation_todo\n}\n\nfragment Todo_user on User {\n ...ChangeTodoStatusMutation_user\n ...RemoveTodoMutation_user\n}\n", "aba626ea9bdf465954e89e5590eb2c1a": "mutation RemoveTodoMutation(\n $input: RemoveTodoInput!\n) {\n removeTodo(input: $input) {\n deletedTodoId\n user {\n completedCount\n totalCount\n id\n }\n }\n}\n", - "ac90c59b3a07237ee6f3cf60f9879f90": "mutation AddTodoMutation(\n $input: AddTodoInput!\n) {\n addTodo(input: $input) {\n todoEdge {\n node {\n complete\n id\n text\n }\n }\n user {\n id\n totalCount\n }\n }\n}\n", "d7dda774dcfa32fe0d9661e01cac9a4a": "mutation ChangeTodoStatusMutation(\n $input: ChangeTodoStatusInput!\n) {\n changeTodoStatus(input: $input) {\n todo {\n id\n complete\n }\n user {\n id\n completedCount\n }\n }\n}\n", "d970fd7dbf118794415dec7324d463e3": "mutation RenameTodoMutation(\n $input: RenameTodoInput!\n) {\n renameTodo(input: $input) {\n todo {\n id\n text\n }\n }\n}\n", "db9904c31d91416f21d45fe3d153884c": "mutation MarkAllTodosMutation(\n $input: MarkAllTodosInput!\n) {\n markAllTodos(input: $input) {\n changedTodos {\n id\n complete\n }\n user {\n id\n completedCount\n }\n }\n}\n" diff --git a/todo/__generated__/relay/AddTodoMutation.graphql.js b/todo/__generated__/relay/AddTodoMutation.graphql.js index 79782f8c..50f21f25 100644 --- a/todo/__generated__/relay/AddTodoMutation.graphql.js +++ b/todo/__generated__/relay/AddTodoMutation.graphql.js @@ -1,6 +1,6 @@ /** - * @generated SignedSource<<5c79c20b3cccc50deaeb29131ca183e4>> - * @relayHash ac90c59b3a07237ee6f3cf60f9879f90 + * @generated SignedSource<<8866b2081f7e08f67db6388d76979196>> + * @relayHash 771fc5cec159852d7952e9a29ae605b4 * @flow * @lightSyntaxTransform * @nogrep @@ -10,7 +10,7 @@ 'use strict'; -// @relayRequestID ac90c59b3a07237ee6f3cf60f9879f90 +// @relayRequestID 771fc5cec159852d7952e9a29ae605b4 /*:: import type { ConcreteRequest, Mutation } from 'relay-runtime'; @@ -27,7 +27,6 @@ export type AddTodoMutation$data = {| +addTodo: ?{| +todoEdge: {| +node: ?{| - +complete: boolean, +id: string, +text: string, |}, @@ -87,13 +86,6 @@ v3 = { "name": "node", "plural": false, "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "complete", - "storageKey": null - }, (v2/*: any*/), { "alias": null, @@ -189,7 +181,7 @@ return { ] }, "params": { - "id": "ac90c59b3a07237ee6f3cf60f9879f90", + "id": "771fc5cec159852d7952e9a29ae605b4", "metadata": {}, "name": "AddTodoMutation", "operationKind": "mutation", @@ -198,7 +190,7 @@ return { }; })(); -(node/*: any*/).hash = "4ae75ce7e6724d608803b8cdd76c087f"; +(node/*: any*/).hash = "70809d52bdd63cf92c37567115ef50e7"; module.exports = ((node/*: any*/)/*: Mutation< AddTodoMutation$variables, diff --git a/todo/__generated__/relay/TodoAppQuery.graphql.js b/todo/__generated__/relay/TodoAppQuery.graphql.js index 3de5d133..2c080145 100644 --- a/todo/__generated__/relay/TodoAppQuery.graphql.js +++ b/todo/__generated__/relay/TodoAppQuery.graphql.js @@ -1,6 +1,6 @@ /** - * @generated SignedSource<<1eb563a36ecae92cf484e8b1db6af4df>> - * @relayHash 8a75762b2e92a08aeb1007fc11f71298 + * @generated SignedSource<<1313c06670e5439592f51142bd7ccf9a>> + * @relayHash 85b55396fb97187f9eeb6b8511ad5de7 * @flow * @lightSyntaxTransform * @nogrep @@ -10,16 +10,25 @@ 'use strict'; -// @relayRequestID 8a75762b2e92a08aeb1007fc11f71298 +// @relayRequestID 85b55396fb97187f9eeb6b8511ad5de7 /*:: import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { TodoList_todos$fragmentType } from "./TodoList_todos.graphql"; import type { TodoList_user$fragmentType } from "./TodoList_user.graphql"; export type TodoAppQuery$variables = {| userId?: ?string, |}; export type TodoAppQuery$data = {| +user: {| + +todos: ?{| + +edges: ?$ReadOnlyArray, + +$fragmentSpreads: TodoList_todos$fragmentType, + |}, +$fragmentSpreads: TodoList_user$fragmentType, |}, |}; @@ -44,20 +53,59 @@ v1 = [ "variableName": "userId" } ], -v2 = [ - { - "kind": "Literal", - "name": "first", - "value": 2147483647 - } -], -v3 = { +v2 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null -}; +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null +}, +v6 = [ + { + "kind": "Literal", + "name": "first", + "value": 2147483647 + } +]; return { "fragment": { "argumentDefinitions": (v0/*: any*/), @@ -75,6 +123,48 @@ return { "name": "user", "plural": false, "selections": [ + { + "alias": "todos", + "args": null, + "concreteType": "TodoConnection", + "kind": "LinkedField", + "name": "__TodoApp_todos_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TodoEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Todo", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + }, + (v4/*: any*/) + ], + "storageKey": null + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "TodoList_todos" + }, + (v5/*: any*/) + ], + "storageKey": null + }, { "args": null, "kind": "FragmentSpread", @@ -106,7 +196,7 @@ return { "selections": [ { "alias": null, - "args": (v2/*: any*/), + "args": (v6/*: any*/), "concreteType": "TodoConnection", "kind": "LinkedField", "name": "todos", @@ -128,6 +218,7 @@ return { "name": "node", "plural": false, "selections": [ + (v2/*: any*/), (v3/*: any*/), { "alias": null, @@ -142,24 +233,11 @@ return { "kind": "ScalarField", "name": "text", "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null } ], "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } + (v4/*: any*/) ], "storageKey": null }, @@ -175,40 +253,16 @@ return { } ] }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - } - ], - "storageKey": null - } + (v5/*: any*/) ], "storageKey": "todos(first:2147483647)" }, { "alias": null, - "args": (v2/*: any*/), + "args": (v6/*: any*/), "filters": null, "handle": "connection", - "key": "TodoList_todos", + "key": "TodoApp_todos", "kind": "LinkedHandle", "name": "todos" }, @@ -233,15 +287,27 @@ return { "name": "userId", "storageKey": null }, - (v3/*: any*/) + (v2/*: any*/) ], "storageKey": null } ] }, "params": { - "id": "8a75762b2e92a08aeb1007fc11f71298", - "metadata": {}, + "id": "85b55396fb97187f9eeb6b8511ad5de7", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "user", + "todos" + ] + } + ] + }, "name": "TodoAppQuery", "operationKind": "query", "text": null @@ -249,7 +315,7 @@ return { }; })(); -(node/*: any*/).hash = "021cf002a0c5c39c772369311b469cec"; +(node/*: any*/).hash = "743e9c91a106f708cbe79c5cc65c934b"; require('relay-runtime').PreloadableQueryRegistry.set((node.params/*: any*/).id, node); diff --git a/todo/__generated__/relay/TodoList_todos.graphql.js b/todo/__generated__/relay/TodoList_todos.graphql.js new file mode 100644 index 00000000..ec6a17a7 --- /dev/null +++ b/todo/__generated__/relay/TodoList_todos.graphql.js @@ -0,0 +1,110 @@ +/** + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { MarkAllTodosMutation_todoEdge$fragmentType } from "./MarkAllTodosMutation_todoEdge.graphql"; +import type { TodoListFooter_todoConnection$fragmentType } from "./TodoListFooter_todoConnection.graphql"; +import type { Todo_todo$fragmentType } from "./Todo_todo.graphql"; +import type { FragmentType } from "relay-runtime"; +declare export opaque type TodoList_todos$fragmentType: FragmentType; +export type TodoList_todos$data = {| + +__id: string, + +edges: ?$ReadOnlyArray, + +$fragmentSpreads: TodoListFooter_todoConnection$fragmentType, + +$fragmentType: TodoList_todos$fragmentType, +|}; +export type TodoList_todos$key = { + +$data?: TodoList_todos$data, + +$fragmentSpreads: TodoList_todos$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "TodoList_todos", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TodoEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Todo", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "Todo_todo" + } + ], + "storageKey": null + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "MarkAllTodosMutation_todoEdge" + } + ], + "storageKey": null + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "TodoListFooter_todoConnection" + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "type": "TodoConnection", + "abstractKey": null +}; + +(node/*: any*/).hash = "77ce67f603af226d50b62ad8e3471d1f"; + +module.exports = ((node/*: any*/)/*: Fragment< + TodoList_todos$fragmentType, + TodoList_todos$data, +>*/); diff --git a/todo/__generated__/relay/TodoList_user.graphql.js b/todo/__generated__/relay/TodoList_user.graphql.js index 1c57dc89..5e7c1a4d 100644 --- a/todo/__generated__/relay/TodoList_user.graphql.js +++ b/todo/__generated__/relay/TodoList_user.graphql.js @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<034129d86bd6ad50f15aed1303def6a4>> + * @generated SignedSource<<22ea1f8169227fc4903749f8c28746ac>> * @flow * @lightSyntaxTransform * @nogrep @@ -12,27 +12,13 @@ /*:: import type { Fragment, ReaderFragment } from 'relay-runtime'; import type { AddTodoMutation_user$fragmentType } from "./AddTodoMutation_user.graphql"; -import type { MarkAllTodosMutation_todoEdge$fragmentType } from "./MarkAllTodosMutation_todoEdge.graphql"; import type { MarkAllTodosMutation_user$fragmentType } from "./MarkAllTodosMutation_user.graphql"; -import type { TodoListFooter_todoConnection$fragmentType } from "./TodoListFooter_todoConnection.graphql"; import type { TodoListFooter_user$fragmentType } from "./TodoListFooter_user.graphql"; -import type { Todo_todo$fragmentType } from "./Todo_todo.graphql"; import type { Todo_user$fragmentType } from "./Todo_user.graphql"; import type { FragmentType } from "relay-runtime"; declare export opaque type TodoList_user$fragmentType: FragmentType; export type TodoList_user$data = {| +completedCount: number, - +todos: ?{| - +__id: string, - +edges: ?$ReadOnlyArray, - +$fragmentSpreads: TodoListFooter_todoConnection$fragmentType, - |}, +totalCount: number, +$fragmentSpreads: AddTodoMutation_user$fragmentType & MarkAllTodosMutation_user$fragmentType & TodoListFooter_user$fragmentType & Todo_user$fragmentType, +$fragmentType: TodoList_user$fragmentType, @@ -47,126 +33,9 @@ export type TodoList_user$key = { var node/*: ReaderFragment*/ = { "argumentDefinitions": [], "kind": "Fragment", - "metadata": { - "connection": [ - { - "count": null, - "cursor": null, - "direction": "forward", - "path": [ - "todos" - ] - } - ] - }, + "metadata": null, "name": "TodoList_user", "selections": [ - { - "alias": "todos", - "args": null, - "concreteType": "TodoConnection", - "kind": "LinkedField", - "name": "__TodoList_todos_connection", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "TodoEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Todo", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "args": null, - "kind": "FragmentSpread", - "name": "Todo_todo" - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - } - ], - "storageKey": null - }, - { - "args": null, - "kind": "FragmentSpread", - "name": "MarkAllTodosMutation_todoEdge" - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } - ], - "storageKey": null - }, - { - "args": null, - "kind": "FragmentSpread", - "name": "TodoListFooter_todoConnection" - }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - } - ], - "storageKey": null - }, - { - "kind": "ClientExtension", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__id", - "storageKey": null - } - ] - } - ], - "storageKey": null - }, { "alias": null, "args": null, @@ -206,7 +75,7 @@ var node/*: ReaderFragment*/ = { "abstractKey": null }; -(node/*: any*/).hash = "c83e0fccfae7ee28c5b4994bd7caae65"; +(node/*: any*/).hash = "e006438298d8a2ad11c51d62ea9680a2"; module.exports = ((node/*: any*/)/*: Fragment< TodoList_user$fragmentType, diff --git a/todo/js/app.js b/todo/js/app.js index c458a64e..2c5c3d22 100644 --- a/todo/js/app.js +++ b/todo/js/app.js @@ -21,11 +21,21 @@ import { import {ErrorBoundary} from 'react-error-boundary'; import TodoAppEntryPoint from './entrypoints/TodoApp.entrypoint'; +import {Flags} from './common/flags'; async function fetchQuery( params: RequestParameters, variables: Variables, ): Promise { + if ( + params.name === 'AddTodoMutation' && + variables.input.text === 'rollback' + ) { + return new Promise((_, reject) => + setTimeout(() => reject(new Error('Mocked network error')), 500), + ); + } + const response = await fetch('/graphql', { method: 'POST', headers: { @@ -43,6 +53,22 @@ async function fetchQuery( const modernEnvironment: Environment = new Environment({ network: Network.create(fetchQuery), store: new Store(new RecordSource()), + scheduler: { + cancel: () => {}, + schedule: (fn) => { + if (Flags.IS_BATCH_SCHEDULER_ENABLED) { + // Batching updates mitigates our stale connection bug during optimistic rollback of a successful response. + // However, rollbacks that occur during network errors DO NOT use the task scheduler and are still susceptible + // to this issue. + ReactDOM.unstable_batchedUpdates(() => { + fn(); + }); + } else { + fn(); + } + return ''; + }, + }, }); const rootElement = document.getElementById('root'); diff --git a/todo/js/common/flags.js b/todo/js/common/flags.js new file mode 100644 index 00000000..cf278496 --- /dev/null +++ b/todo/js/common/flags.js @@ -0,0 +1,3 @@ +export const Flags = { + IS_BATCH_SCHEDULER_ENABLED: false, +}; diff --git a/todo/js/components/TodoApp.js b/todo/js/components/TodoApp.js index 10f14573..6d4724ee 100644 --- a/todo/js/components/TodoApp.js +++ b/todo/js/components/TodoApp.js @@ -10,15 +10,31 @@ import TodoList from './TodoList'; import * as React from 'react'; import {graphql, usePreloadedQuery} from 'react-relay'; +import {Flags} from '../common/flags'; type PreloadedQueries = {|todoAppQueryRef: PreloadedQuery|}; type Props = EntryPointProps; export default (function TodoApp({queries}: Props): React.Node { + const [isBatchedSchedulerEnabled, setIsBatchSchedulerEnabled] = + React.useState(Flags.IS_BATCH_SCHEDULER_ENABLED); + + const [listKey, setListKey] = React.useState(0); const {user} = usePreloadedQuery( graphql` query TodoAppQuery($userId: String) @preloadable { user(id: $userId) @required(action: THROW) { + todos( + first: 2147483647 # max GraphQLInt + ) @connection(key: "TodoApp_todos") { + edges { + # Read from the connection to ensure the app re-renders when appending a new edge + node { + id + } + } + ...TodoList_todos + } ...TodoList_user } } @@ -26,12 +42,31 @@ export default (function TodoApp({queries}: Props): React.Node { queries.todoAppQueryRef, ); + React.useEffect(() => { + // Force a remount of the TodoList so it appears AFTER TodoApp in Relay's subscription set. When the connection is + // updated this will ensure the TodoApp is rendered BEFORE TodoList as subscription notifications are pushed, + // rendering the app children which still reference the stale connection data. + setListKey((prev) => prev + 1); + }, []); + return (
- +
+ +