diff --git a/__tests__/ReactRelayFragmentContainer-test.tsx b/__tests__/ReactRelayFragmentContainer-test.tsx index c592e940..41633f0a 100644 --- a/__tests__/ReactRelayFragmentContainer-test.tsx +++ b/__tests__/ReactRelayFragmentContainer-test.tsx @@ -14,22 +14,16 @@ import * as React from 'react'; import * as ReactTestRenderer from 'react-test-renderer'; -import { - useQuery, - useFragment, - RelayEnvironmentProvider, - useRelayEnvironment, - NETWORK_ONLY, -} from '../src'; +import { useQuery, useFragment, RelayEnvironmentProvider, useRelayEnvironment, NETWORK_ONLY } from '../src'; function createHooks(component, options?: any) { - const result = ReactTestRenderer.create(component, options); + let result; ReactTestRenderer.act(() => { + result = ReactTestRenderer.create(component, options); jest.runAllImmediates(); }); return result; } - const ReactRelayFragmentContainer = { createContainer: (Component, spec) => (props) => { const { user, ...others } = props; @@ -127,10 +121,7 @@ describe('ReactRelayFragmentContainer', () => { `; UserQueryWithCond = graphql` - query ReactRelayFragmentContainerTestUserWithCondQuery( - $id: ID! - $condGlobal: Boolean! - ) { + query ReactRelayFragmentContainerTestUserWithCondQuery($id: ID!, $condGlobal: Boolean!) { node(id: $id) { ...ReactRelayFragmentContainerTestUserFragment @arguments(cond: $condGlobal) } @@ -138,7 +129,7 @@ describe('ReactRelayFragmentContainer', () => { `; UserFragment = graphql` fragment ReactRelayFragmentContainerTestUserFragment on User - @argumentDefinitions(cond: { type: "Boolean!", defaultValue: true }) { + @argumentDefinitions(cond: { type: "Boolean!", defaultValue: true }) { id name @include(if: $cond) } @@ -338,12 +329,7 @@ describe('ReactRelayFragmentContainer', () => { missingClientEdges: null, missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '842472', - { cond: true }, - ownerUser2.request, - ), + selector: createReaderSelector(UserFragment, '842472', { cond: true }, ownerUser2.request), }); }); @@ -358,8 +344,7 @@ describe('ReactRelayFragmentContainer', () => { environment.lookup.mockClear(); environment.subscribe.mockClear(); - userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data - .node; + userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data.node; instance.getInstance().setProps({ user: userPointer, }); @@ -389,12 +374,7 @@ describe('ReactRelayFragmentContainer', () => { missingClientEdges: null, missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '4', - { cond: false }, - ownerUser1WithCondVar.request, - ), + selector: createReaderSelector(UserFragment, '4', { cond: false }, ownerUser1WithCondVar.request), }); }); diff --git a/__tests__/ReactRelayPaginationContainer-test.tsx b/__tests__/ReactRelayPaginationContainer-test.tsx index 8bf00c56..5bcdddcc 100644 --- a/__tests__/ReactRelayPaginationContainer-test.tsx +++ b/__tests__/ReactRelayPaginationContainer-test.tsx @@ -18,8 +18,9 @@ import { usePagination, RelayEnvironmentProvider, useRelayEnvironment } from '.. import { forceCache } from '../src/Utils'; function createHooks(component, options?: any) { - const result = ReactTestRenderer.create(component, options); + let result; ReactTestRenderer.act(() => { + result = ReactTestRenderer.create(component, options); jest.runAllImmediates(); }); return result; @@ -165,18 +166,14 @@ describe('ReactRelayPaginationContainer', () => { UserFragment = graphql` fragment ReactRelayPaginationContainerTestUserFragment on User - @refetchable(queryName: "ReactRelayPaginationContainerUserFragmentRefetchQuery") - @argumentDefinitions( - isViewerFriendLocal: { type: "Boolean", defaultValue: false } - orderby: { type: "[String]" } - ) { + @refetchable(queryName: "ReactRelayPaginationContainerUserFragmentRefetchQuery") + @argumentDefinitions( + isViewerFriendLocal: { type: "Boolean", defaultValue: false } + orderby: { type: "[String]" } + ) { id - friends( - after: $after - first: $count - orderby: $orderby - isViewerFriend: $isViewerFriendLocal - ) @connection(key: "UserFragment_friends") { + friends(after: $after, first: $count, orderby: $orderby, isViewerFriend: $isViewerFriendLocal) + @connection(key: "UserFragment_friends") { edges { node { id @@ -533,8 +530,7 @@ describe('ReactRelayPaginationContainer', () => { environment.lookup.mockClear(); environment.subscribe.mockClear(); - userPointer = environment.lookup(ownerUser1WithOtherVar.fragment, ownerUser1WithOtherVar) - .data.node; + userPointer = environment.lookup(ownerUser1WithOtherVar.fragment, ownerUser1WithOtherVar).data.node; instance.getInstance().setProps({ user: userPointer, }); @@ -637,8 +633,7 @@ describe('ReactRelayPaginationContainer', () => { environment.subscribe.mockClear(); // Pass an updated user pointer that references different variables - userPointer = environment.lookup(ownerUser1WithOtherVar.fragment, ownerUser1WithOtherVar) - .data.node; + userPointer = environment.lookup(ownerUser1WithOtherVar.fragment, ownerUser1WithOtherVar).data.node; instance.getInstance().setProps({ user: userPointer, }); @@ -909,11 +904,8 @@ describe('ReactRelayPaginationContainer', () => { `; UserFragment = graphql` fragment ReactRelayPaginationContainerTestNoConnectionOnFragmentUserFragment on User - @refetchable( - queryName: "ReactRelayPaginationContainerTestNoConnectionUserFragmentRefetchQuery" - ) { - friends(after: $after, first: $count, orderby: $orderby) - @connection(key: "UserFragment_friends") { + @refetchable(queryName: "ReactRelayPaginationContainerTestNoConnectionUserFragmentRefetchQuery") { + friends(after: $after, first: $count, orderby: $orderby) @connection(key: "UserFragment_friends") { edges { node { id @@ -1129,11 +1121,14 @@ describe('ReactRelayPaginationContainer', () => { it('updates after pagination (if more results)', () => { const userPointer = environment.lookup(ownerUser1.fragment, ownerUser1).data.node; - ReactTestRenderer.create( - - - , - ); + ReactTestRenderer.act(() => { + ReactTestRenderer.create( + + + , + ); + jest.runAllImmediates(); + }); expect(render.mock.calls.length).toBe(1); expect(render.mock.calls[0][0].user.friends.edges.length).toBe(1); @@ -1168,8 +1163,6 @@ describe('ReactRelayPaginationContainer', () => { expect(render.mock.calls.length).toBe(3); expect(render.mock.calls[0][0].user.friends.edges.length).toBe(1); expect(render.mock.calls[0][0].relay.isLoadingNext).toBe(true); - expect(render.mock.calls[1][0].user.friends.edges.length).toBe(2); - expect(render.mock.calls[1][0].relay.isLoadingNext).toBe(true); expect(render.mock.calls[2][0].user.friends.edges.length).toBe(2); expect(render.mock.calls[2][0].relay.isLoadingNext).toBe(false); expect(render.mock.calls[2][0].relay.hasMore).toBe(true); @@ -1177,11 +1170,14 @@ describe('ReactRelayPaginationContainer', () => { it('updates after pagination (if no more results)', () => { const userPointer = environment.lookup(ownerUser1.fragment, ownerUser1).data.node; - ReactTestRenderer.create( - - - , - ); + ReactTestRenderer.act(() => { + ReactTestRenderer.create( + + + , + ); + jest.runAllImmediates(); + }); expect(render.mock.calls.length).toBe(1); expect(render.mock.calls[0][0].user.friends.edges.length).toBe(1); @@ -1216,8 +1212,6 @@ describe('ReactRelayPaginationContainer', () => { expect(render.mock.calls.length).toBe(3); expect(render.mock.calls[0][0].user.friends.edges.length).toBe(1); expect(render.mock.calls[0][0].relay.isLoadingNext).toBe(true); - expect(render.mock.calls[1][0].user.friends.edges.length).toBe(2); - expect(render.mock.calls[1][0].relay.isLoadingNext).toBe(true); expect(render.mock.calls[2][0].user.friends.edges.length).toBe(2); expect(render.mock.calls[2][0].relay.isLoadingNext).toBe(false); expect(render.mock.calls[2][0].relay.hasMore).toBe(false); @@ -1402,9 +1396,7 @@ describe('ReactRelayPaginationContainer', () => { isViewerFriendLocal: false, }; loadMore(1, jest.fn()); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, variables, forceCache), - ).toBe(true); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, variables, forceCache)).toBe(true); }); it('calls the callback when the fetch succeeds', () => { @@ -1611,9 +1603,7 @@ describe('ReactRelayPaginationContainer', () => { isViewerFriendLocal: false, }; refetchConnection(1, jest.fn()); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, variables, forceCache), - ).toBe(true); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, variables, forceCache)).toBe(true); }); it('calls the callback when the fetch succeeds', () => { @@ -1867,9 +1857,9 @@ describe('ReactRelayPaginationContainer', () => { }); expect(references.length).toBe(1); expect(references[0].dispose).not.toBeCalled(); - expect(render.mock.calls.length).toBe(4); - expect(render.mock.calls[3][0].user.friends.edges.length).toBe(1); - expect(render.mock.calls[3][0]).toEqual({ + expect(render.mock.calls.length).toBe(3); + expect(render.mock.calls[2][0].user.friends.edges.length).toBe(1); + expect(render.mock.calls[2][0]).toEqual({ user: { id: '4', friends: { @@ -1937,9 +1927,7 @@ describe('ReactRelayPaginationContainer', () => { isViewerFriendLocal: true, id: '4', }; - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, variables, forceCache), - ).toBe(true); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, variables, forceCache)).toBe(true); }); it('should not refetch connection if container is unmounted', () => { diff --git a/__tests__/ReactRelayQueryRenderer-test.tsx b/__tests__/ReactRelayQueryRenderer-test.tsx index c62f2767..528bbeb0 100644 --- a/__tests__/ReactRelayQueryRenderer-test.tsx +++ b/__tests__/ReactRelayQueryRenderer-test.tsx @@ -23,8 +23,9 @@ import * as ReactTestRenderer from 'react-test-renderer'; //import readContext from "react-relay/lib/readContext"; function createHooks(component, options?: any) { - const result = ReactTestRenderer.create(component, options); + let result; ReactTestRenderer.act(() => { + result = ReactTestRenderer.create(component, options); jest.runAllImmediates(); }); return result; @@ -63,7 +64,7 @@ const loadingState = { isLoading: true, }; -function expectToBeRendered(render, readyState) { +function expectToBeRenderedStore(render, readyState) { const calls = render.mock.calls; // Ensure useEffect is called before other timers expect(calls.length).toBe(2); @@ -72,6 +73,13 @@ function expectToBeRendered(render, readyState) { return { pass: true }; } +function expectToBeRendered(render, readyState, isLoading = false) { + const calls = render.mock.calls; + // Ensure useEffect is called before other timers + expect(calls.length).toBe(1); + expect(calls[0][0]).toEqual({ ...readyState, isLoading }); + return { pass: true }; +} const QueryRendererHook = (props) => { const { render, @@ -83,6 +91,7 @@ const QueryRendererHook = (props) => { skip, onComplete, onResponse, + UNSTABLE_renderPolicy, } = props; const relays = useQuery(query, variables, { networkCacheConfig: cacheConfig, @@ -91,6 +100,7 @@ const QueryRendererHook = (props) => { skip, onComplete, onResponse, + UNSTABLE_renderPolicy, }); return {render(relays)}; @@ -105,6 +115,7 @@ const ReactRelayQueryRenderer = (props) => ( describe('ReactRelayQueryRenderer', () => { let TestQuery; let NextQuery; + let CompleteTestQuery; let cacheConfig; let environment; let render; @@ -121,6 +132,17 @@ describe('ReactRelayQueryRenderer', () => { }, }; + const responseComplete = { + data: { + node: { + __typename: 'User', + id: '4', + name: 'Zuck', + username: 'Zuck', + }, + }, + }; + const responseErrors = { data: { node: { @@ -195,6 +217,17 @@ describe('ReactRelayQueryRenderer', () => { } `; + CompleteTestQuery = graphql` + query ReactRelayQueryRendererTestCompleteQuery($id: ID = "") { + node(id: $id) { + id + username + name + ...ReactRelayQueryRendererTestFragment + } + } + `; + NextQuery = graphql` query ReactRelayQueryRendererTestNextQuery($id: ID!) { node(id: $id) { @@ -220,6 +253,198 @@ describe('ReactRelayQueryRenderer', () => { }); describe('when initialized', () => { + it('check store and network', () => { + environment.mockClear(); + environment.commitUpdate((_store) => { + let root = _store.get(ROOT_ID); + if (!root) { + root = _store.create(ROOT_ID, ROOT_TYPE); + } + const user = _store.create('4', 'User'); + user.setValue('4', 'id'); + user.setValue('Zuck', 'name'); + user.setValue('Zuck', 'username'); + root.setLinkedRecord(user, 'node', { id: '4' }); + }); + createHooks( + , + ); + + expect(environment.execute.mock.calls.length).toBe(1); + const owner = createOperationDescriptor(CompleteTestQuery, variables); + expectToBeRendered( + render, + { + error: null, + data: { + node: { + id: '4', + name: 'Zuck', + username: 'Zuck', + __isWithinUnmatchedTypeRefinement: false, + __id: '4', + __fragments: { + ReactRelayQueryRendererTestFragment: {}, + }, + __fragmentOwner: owner.request, + }, + }, + retry: expect.any(Function), + }, + true, + ); + render.mockClear(); + environment.mock.resolve(CompleteTestQuery, responseComplete); + expectToBeRendered(render, { + error: null, + data: { + node: { + id: '4', + name: 'Zuck', + username: 'Zuck', + __isWithinUnmatchedTypeRefinement: false, + __id: '4', + __fragments: { + ReactRelayQueryRendererTestFragment: {}, + }, + __fragmentOwner: owner.request, + }, + }, + retry: expect.any(Function), + }); + }); + + it('check store and network partial', () => { + environment.mockClear(); + environment.commitUpdate((_store) => { + let root = _store.get(ROOT_ID); + if (!root) { + root = _store.create(ROOT_ID, ROOT_TYPE); + } + const user = _store.create('4', 'User'); + user.setValue('4', 'id'); + user.setValue('Zuck', 'name'); + root.setLinkedRecord(user, 'node', { id: '4' }); + }); + createHooks( + , + ); + + expect(environment.execute.mock.calls.length).toBe(1); + const owner = createOperationDescriptor(CompleteTestQuery, variables); + expectToBeRendered( + render, + { + error: null, + data: { + node: { + id: '4', + name: 'Zuck', + username: undefined, + __isWithinUnmatchedTypeRefinement: false, + __id: '4', + __fragments: { + ReactRelayQueryRendererTestFragment: {}, + }, + __fragmentOwner: owner.request, + }, + }, + retry: expect.any(Function), + }, + true, + ); + render.mockClear(); + environment.mock.resolve(CompleteTestQuery, responseComplete); + expectToBeRenderedStore(render, { + error: null, + data: { + node: { + id: '4', + name: 'Zuck', + username: 'Zuck', + __isWithinUnmatchedTypeRefinement: false, + __id: '4', + __fragments: { + ReactRelayQueryRendererTestFragment: {}, + }, + __fragmentOwner: owner.request, + }, + }, + retry: expect.any(Function), + }); + }); + /* + it('observe query polling store and network', () => { + const onComplete = jest.fn(() => undefined); + const newCacheConfig = { + ...cacheConfig, + poll: 1, + }; + environment.mockClear(); + environment.commitUpdate((_store) => { + let root = _store.get(ROOT_ID); + if (!root) { + root = _store.create(ROOT_ID, ROOT_TYPE); + } + const user = _store.create('4', 'User'); + user.setValue('4', 'id'); + user.setValue('Zuck', 'name'); + root.setLinkedRecord(user, 'node', { id: '4' }); + }); + createHooks( + + + , + ); + + expect(environment.execute.mock.calls.length).toBe(1); + expect(onComplete).not.toBeCalled(); + render.mockClear(); + environment.mock.resolve(TestQuery, response); + expect(onComplete).not.toBeCalled(); + const owner = createOperationDescriptor(TestQuery, variables, newCacheConfig); + expectToBeRendered(render, { + error: null, + data: { + node: { + id: '4', + __isWithinUnmatchedTypeRefinement: false, + + __fragments: { + ReactRelayQueryRendererTestFragment: {}, + }, + + __fragmentOwner: owner.request, + __id: '4', + }, + }, + retry: expect.any(Function), + }); + }); +*/ it('skip', () => { const renderer = createHooks( @@ -321,6 +546,50 @@ describe('ReactRelayQueryRenderer', () => { }); }); + it('observe query polling', () => { + const onComplete = jest.fn(() => undefined); + const newCacheConfig = { + ...cacheConfig, + poll: 1, + }; + createHooks( + + + , + ); + + expect(environment.execute.mock.calls.length).toBe(1); + expect(onComplete).not.toBeCalled(); + render.mockClear(); + environment.mock.resolve(TestQuery, response); + expect(onComplete).not.toBeCalled(); + const owner = createOperationDescriptor(TestQuery, variables, newCacheConfig); + expectToBeRendered(render, { + error: null, + data: { + node: { + id: '4', + __isWithinUnmatchedTypeRefinement: false, + + __fragments: { + ReactRelayQueryRendererTestFragment: {}, + }, + + __fragmentOwner: owner.request, + __id: '4', + }, + }, + retry: expect.any(Function), + }); + }); + describe('when constructor fires multiple times', () => { describe('when store does not have snapshot and fetch does not return snapshot', () => { it('fetches the query only once, renders loading state', () => { @@ -358,11 +627,10 @@ describe('ReactRelayQueryRenderer', () => { unstable_concurrentUpdatesByDefault: true, }); }); - + // Flush some of the changes, but don't commit (Scheduler as any).unstable_flushNumberOfYields(2); - expect((Scheduler as any).unstable_clearYields()).toEqual(['A', 'B']); expect(renderer.toJSON()).toEqual(null); expect().loadingRendered(); @@ -373,7 +641,6 @@ describe('ReactRelayQueryRenderer', () => { renderer.update(); }); - expect(environment.execute.mock.calls.length).toBe(1); expect().loadingRendered(); }); @@ -665,8 +932,8 @@ describe('ReactRelayQueryRenderer', () => { // request thirdRequest.resolve(thirdResponse); secondRequest.resolve(secondResponse); - expect(render.mock.calls.length).toEqual(4); - const lastRender = render.mock.calls[3][0]; + expect(render.mock.calls.length).toEqual(3); + const lastRender = render.mock.calls[2][0]; expect(lastRender).toEqual({ error: null, data: { @@ -1138,9 +1405,7 @@ describe('ReactRelayQueryRenderer', () => { render, variables, }); - expect(environment.mock.isLoading(TestQuery, expectedVariables, cacheConfig)).toBe( - true, - ); + expect(environment.mock.isLoading(TestQuery, expectedVariables, cacheConfig)).toBe(true); expect().loadingRendered(); }); @@ -1250,7 +1515,7 @@ describe('ReactRelayQueryRenderer', () => { }); it('refetch the query if `retry`', () => { - expect.assertions(7); + expect.assertions(6); render.mockClear(); const error = new Error('network fails'); environment.mock.reject(TestQuery, error); @@ -1313,7 +1578,7 @@ describe('ReactRelayQueryRenderer', () => { } } const renderer = createHooks(); - expect.assertions(7); + expect.assertions(5); mockA.mockClear(); mockB.mockClear(); environment.mock.resolve(TestQuery, response); @@ -1384,7 +1649,7 @@ describe('ReactRelayQueryRenderer', () => { }); it('renders the query results', () => { - expect.assertions(3); + expect.assertions(2); render.mockClear(); environment.mock.resolve(TestQuery, response); const owner = createOperationDescriptor(TestQuery, variables); @@ -1848,7 +2113,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); environment.mock.resolve(TestQuery, response); - expect(render).toBeCalledTimes(2); + expect(render).toBeCalledTimes(1); const readyState = render.mock.calls[0][0]; expect(readyState.retry).not.toBe(null); environment.mockClear(); @@ -1888,7 +2153,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); environment.mock.resolve(TestQuery, response); - expect(render).toBeCalledTimes(2); + expect(render).toBeCalledTimes(1); const readyState = render.mock.calls[0][0]; expect(readyState.retry).not.toBe(null); environment.mockClear(); @@ -1916,7 +2181,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); environment.mock.resolve(TestQuery, response); - expect(render).toBeCalledTimes(2); + expect(render).toBeCalledTimes(1); const readyState = render.mock.calls[0][0]; expect(readyState.retry).not.toBe(null); environment.mockClear(); @@ -1986,8 +2251,8 @@ describe('ReactRelayQueryRenderer', () => { let response = null; const onResponse = (res) => { response = res; - console.log('onResponse test', res); }; + createHooks( { onResponse={onResponse} />, ); - expect.assertions(4); + + expect.assertions(3); + render.mockClear(); environment.mock.resolve(TestQuery, responseErrors); const owner = createOperationDescriptor(TestQuery, variables); diff --git a/__tests__/ReactRelayRefetchContainer-test.tsx b/__tests__/ReactRelayRefetchContainer-test.tsx index b4e5a6e5..b771796e 100644 --- a/__tests__/ReactRelayRefetchContainer-test.tsx +++ b/__tests__/ReactRelayRefetchContainer-test.tsx @@ -12,16 +12,13 @@ /* eslint-disable */ import * as React from 'react'; import * as ReactTestRenderer from 'react-test-renderer'; -import { - useRefetchable as useRefetch, - RelayEnvironmentProvider, - useRelayEnvironment, -} from '../src'; +import { useRefetchable as useRefetch, RelayEnvironmentProvider, useRelayEnvironment } from '../src'; import { forceCache } from '../src/Utils'; -function createHooks(component) { - const result = ReactTestRenderer.create(component); +function createHooks(component, options?: any) { + let result; ReactTestRenderer.act(() => { + result = ReactTestRenderer.create(component, options); jest.runAllImmediates(); }); return result; @@ -38,14 +35,7 @@ const ReactRelayRefetchContainer = { fetchPolicy: options?.fetchPolicy, }); }; - return ( - - ); + return ; }, }; @@ -137,8 +127,8 @@ describe('ReactRelayRefetchContainer', () => { `; UserFragment = graphql` fragment ReactRelayRefetchContainerTestUserFragment on User - @refetchable(queryName: "ReactRelayRefetchContainerTestUserFragmentRefetchQuery") - @argumentDefinitions(cond: { type: "Boolean", defaultValue: true }) { + @refetchable(queryName: "ReactRelayRefetchContainerTestUserFragmentRefetchQuery") + @argumentDefinitions(cond: { type: "Boolean", defaultValue: true }) { id name @include(if: $cond) } @@ -379,12 +369,7 @@ describe('ReactRelayRefetchContainer', () => { missingClientEdges: null, missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '842472', - { cond: true }, - ownerUser2.request, - ), + selector: createReaderSelector(UserFragment, '842472', { cond: true }, ownerUser2.request), }); }); @@ -399,8 +384,7 @@ describe('ReactRelayRefetchContainer', () => { environment.lookup.mockClear(); environment.subscribe.mockClear(); - userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data - .node; + userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data.node; instance.getInstance().setProps({ user: userPointer, }); @@ -432,12 +416,7 @@ describe('ReactRelayRefetchContainer', () => { missingClientEdges: null, missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '4', - { cond: false }, - ownerUser1WithCondVar.request, - ), + selector: createReaderSelector(UserFragment, '4', { cond: false }, ownerUser1WithCondVar.request), }); }); @@ -458,9 +437,7 @@ describe('ReactRelayRefetchContainer', () => { id: '4', }; refetch(refetchVariables, null, jest.fn()); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache), - ).toBe(true); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache)).toBe(true); environment.mock.resolve(UserFragmentRefetchQuery, { data: { node: { @@ -473,8 +450,7 @@ describe('ReactRelayRefetchContainer', () => { environment.subscribe.mockClear(); // Pass an updated user pointer that references different variables - userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data - .node; + userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data.node; instance.getInstance().setProps({ user: userPointer, }); @@ -506,12 +482,7 @@ describe('ReactRelayRefetchContainer', () => { missingClientEdges: null, missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '4', - { cond: false }, - ownerUser1WithCondVar.request, - ), + selector: createReaderSelector(UserFragment, '4', { cond: false }, ownerUser1WithCondVar.request), }); }); @@ -683,9 +654,7 @@ describe('ReactRelayRefetchContainer', () => { id: '4', }; refetch(refetchVariables, null, jest.fn()); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache), - ).toBe(true); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache)).toBe(true); environment.mock.resolve(UserFragmentRefetchQuery, { data: { node: { @@ -707,9 +676,7 @@ describe('ReactRelayRefetchContainer', () => { }; refetch(refetchVariables, null, jest.fn(), refetchOptions); expect(render.mock.calls.length).toBe(2); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache), - ).toBe(false); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache)).toBe(false); expect(environment.execute).toBeCalledTimes(0); }); @@ -819,13 +786,11 @@ describe('ReactRelayRefetchContainer', () => { }, }); refetch(refetchVariables, null, jest.fn()); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, fetchedVariables, forceCache), - ).toBe(false); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, fetchedVariables, forceCache)).toBe(false); }); it('renders with the results of the new variables on success', () => { - expect.assertions(10); + expect.assertions(8); expect(render.mock.calls.length).toBe(1); expect(render.mock.calls[0][0].user.name).toBe('Zuck'); variables = { @@ -845,11 +810,9 @@ describe('ReactRelayRefetchContainer', () => { }, }, }); - expect(render.mock.calls.length).toBe(4); - expect(render.mock.calls[2][0].isLoading).toBe(true); + expect(render.mock.calls.length).toBe(3); + expect(render.mock.calls[2][0].isLoading).toBe(false); expect(render.mock.calls[2][0].user.name).toBe(undefined); - expect(render.mock.calls[3][0].isLoading).toBe(false); - expect(render.mock.calls[3][0].user.name).toBe(undefined); }); it('does not update variables on failure', () => { diff --git a/__tests__/RelayHooks-test.tsx b/__tests__/RelayHooks-test.tsx index a2382623..a283bb84 100644 --- a/__tests__/RelayHooks-test.tsx +++ b/__tests__/RelayHooks-test.tsx @@ -4,18 +4,12 @@ import * as ReactTestRenderer from 'react-test-renderer'; import { createOperationDescriptor, graphql } from 'relay-runtime'; import { createMockEnvironment } from 'relay-test-utils-internal'; -import { - useOssFragment, - RelayEnvironmentProvider, - useRelayEnvironment, - usePagination, - useRefetchable, -} from '../src'; +import { useOssFragment, RelayEnvironmentProvider, useRelayEnvironment, usePagination, useRefetchable } from '../src'; function createHooks(component, options?: any) { - let result;// = ReactTestRenderer.create(component, options); + let result; ReactTestRenderer.act(() => { - result= ReactTestRenderer.create(component, options); + result = ReactTestRenderer.create(component, options); jest.runAllImmediates(); }); return result; @@ -84,18 +78,14 @@ describe('useMemo resolver functions', () => { environment = createMockEnvironment(); UserFragment = graphql` fragment RelayHooksTestUserFragment on User - @refetchable(queryName: "RelayHooksTestUserFragmentRefetchQuery") - @argumentDefinitions( - isViewerFriendLocal: { type: "Boolean", defaultValue: false } - orderby: { type: "[String]" } - ) { + @refetchable(queryName: "RelayHooksTestUserFragmentRefetchQuery") + @argumentDefinitions( + isViewerFriendLocal: { type: "Boolean", defaultValue: false } + orderby: { type: "[String]" } + ) { id - friends( - after: $after - first: $count - orderby: $orderby - isViewerFriend: $isViewerFriendLocal - ) @connection(key: "UserFragment_friends") { + friends(after: $after, first: $count, orderby: $orderby, isViewerFriend: $isViewerFriendLocal) + @connection(key: "UserFragment_friends") { edges { node { id @@ -115,8 +105,7 @@ describe('useMemo resolver functions', () => { node(id: $id) { id __typename - ...RelayHooksTestUserFragment - @arguments(isViewerFriendLocal: $isViewerFriend, orderby: $orderby) + ...RelayHooksTestUserFragment @arguments(isViewerFriendLocal: $isViewerFriend, orderby: $orderby) } } `; @@ -174,20 +163,11 @@ describe('useMemo resolver functions', () => { }; function useOssFragmentJest(fragmentNode, fragmentRef) { - const [data, refetchFunction] = useOssFragment( - fragmentNode, - fragmentRef, - false, - 'useOssFragment', - ); + const [data, refetchFunction] = useOssFragment(fragmentNode, fragmentRef, false, 'useOssFragment'); renderSpy(data, refetchFunction); return [data, refetchFunction]; } - TestContainer = ReactRelayContainer.createContainer( - TestComponent, - UserFragment, - UserQuery, - ); + TestContainer = ReactRelayContainer.createContainer(TestComponent, UserFragment, UserQuery); }); it('re-renders on subscription callbac', () => { const userPointer = environment.lookup(ownerUser1.fragment, ownerUser1).data.node; @@ -281,11 +261,7 @@ describe('useMemo resolver functions', () => { renderSpy(data, refetch); return [data, refetch]; } - TestContainer = ReactRelayContainer.createContainer( - TestComponent, - UserFragment, - UserQuery, - ); + TestContainer = ReactRelayContainer.createContainer(TestComponent, UserFragment, UserQuery); }); it('re-renders on subscription callbac', () => { const userPointer = environment.lookup(ownerUser1.fragment, ownerUser1).data.node; @@ -379,11 +355,7 @@ describe('useMemo resolver functions', () => { renderSpy(data, refetch); return [data, refetch]; } - TestContainer = ReactRelayContainer.createContainer( - TestComponent, - UserFragment, - UserQuery, - ); + TestContainer = ReactRelayContainer.createContainer(TestComponent, UserFragment, UserQuery); }); it('re-renders on subscription callbac', () => { const userPointer = environment.lookup(ownerUser1.fragment, ownerUser1).data.node; diff --git a/__tests__/usePaginationFragment-test.tsx b/__tests__/usePaginationFragment-test.tsx index 60a15f7e..d9dd427d 100644 --- a/__tests__/usePaginationFragment-test.tsx +++ b/__tests__/usePaginationFragment-test.tsx @@ -65,13 +65,7 @@ import * as React from 'react'; import { useMemo, useState } from 'react'; import * as TestRenderer from 'react-test-renderer'; import { getFragment, getRequest, graphql, OperationDescriptor, Variables } from 'relay-runtime'; -import { - ConnectionHandler, - FRAGMENT_OWNER_KEY, - FRAGMENTS_KEY, - ID_KEY, - createOperationDescriptor, -} from 'relay-runtime'; +import { ConnectionHandler, FRAGMENT_OWNER_KEY, FRAGMENTS_KEY, ID_KEY, createOperationDescriptor } from 'relay-runtime'; import { ReactRelayContext, usePaginationFragment as usePaginationFragmentOriginal } from '../src'; const warning = require('fbjs/lib/warning'); @@ -195,12 +189,12 @@ describe('usePaginationFragment', () => { `; gqlFragment = getFragment(graphql` fragment usePaginationFragmentTestUserFragment on User - @refetchable(queryName: "usePaginationFragmentTestUserFragmentPaginationQuery") - @argumentDefinitions( - isViewerFriendLocal: { type: "Boolean", defaultValue: false } - orderby: { type: "[String]" } - scale: { type: "Float" } - ) { + @refetchable(queryName: "usePaginationFragmentTestUserFragmentPaginationQuery") + @argumentDefinitions( + isViewerFriendLocal: { type: "Boolean", defaultValue: false } + orderby: { type: "[String]" } + scale: { type: "Float" } + ) { id name friends( @@ -224,14 +218,12 @@ describe('usePaginationFragment', () => { `); gqlFragmentWithStreaming = getFragment(graphql` fragment usePaginationFragmentTestUserFragmentWithStreaming on User - @refetchable( - queryName: "usePaginationFragmentTestUserFragmentStreamingPaginationQuery" - ) - @argumentDefinitions( - isViewerFriendLocal: { type: "Boolean", defaultValue: false } - orderby: { type: "[String]" } - scale: { type: "Float" } - ) { + @refetchable(queryName: "usePaginationFragmentTestUserFragmentStreamingPaginationQuery") + @argumentDefinitions( + isViewerFriendLocal: { type: "Boolean", defaultValue: false } + orderby: { type: "[String]" } + scale: { type: "Float" } + ) { id name friends( @@ -318,8 +310,7 @@ describe('usePaginationFragment', () => { $last: Int ) { node(id: $id) { - ...usePaginationFragmentTestUserFragment - @arguments(isViewerFriendLocal: true, orderby: ["name"]) + ...usePaginationFragmentTestUserFragment @arguments(isViewerFriendLocal: true, orderby: ["name"]) } } `); @@ -356,30 +347,25 @@ describe('usePaginationFragment', () => { id: '', }; // eslint-disable-next-line prettier/prettier - gqlPaginationQuery = require('./__generated__/usePaginationFragmentTestUserFragmentPaginationQuery.graphql') - .default; + gqlPaginationQuery = + require('./__generated__/usePaginationFragmentTestUserFragmentPaginationQuery.graphql').default; // eslint-disable-next-line prettier/prettier - gqlPaginationQueryWithStreaming = require('./__generated__/usePaginationFragmentTestUserFragmentStreamingPaginationQuery.graphql') - .default; + gqlPaginationQueryWithStreaming = + require('./__generated__/usePaginationFragmentTestUserFragmentStreamingPaginationQuery.graphql').default; invariant( areEqual(gqlFragment.metadata?.refetch?.operation.default, gqlPaginationQuery), 'useRefetchableFragment-test: Expected refetchable fragment metadata to contain operation.', ); invariant( - areEqual( - gqlFragmentWithStreaming.metadata?.refetch?.operation.default, - gqlPaginationQueryWithStreaming, - ), + areEqual(gqlFragmentWithStreaming.metadata?.refetch?.operation.default, gqlPaginationQueryWithStreaming), 'useRefetchableFragment-test: Expected refetchable fragment metadata to contain operation.', ); query = createOperationDescriptor(gqlQuery, variables, { force: true }); - queryNestedFragment = createOperationDescriptor( - gqlQueryNestedFragment, - variablesNestedFragment, - { force: true }, - ); + queryNestedFragment = createOperationDescriptor(gqlQueryNestedFragment, variablesNestedFragment, { + force: true, + }); queryWithoutID = createOperationDescriptor(gqlQueryWithoutID, variablesWithoutID, { force: true, }); @@ -481,9 +467,7 @@ describe('usePaginationFragment', () => { const [owner, _setOwner] = useState(props.owner); const [_, _setCount] = useState(0); const fragment = props.fragment ?? gqlFragment; - const nodeUserRef = useMemo(() => environment.lookup(owner.fragment).data?.node, [ - owner, - ]); + const nodeUserRef = useMemo(() => environment.lookup(owner.fragment).data?.node, [owner]); const ownerOperationRef = useMemo( () => ({ [ID_KEY]: owner.request.variables.id ?? owner.request.variables.nodeID, @@ -494,9 +478,7 @@ describe('usePaginationFragment', () => { }), [owner, fragment.name], ); - const userRef = props.hasOwnProperty('userRef') - ? props.userRef - : nodeUserRef ?? ownerOperationRef; + const userRef = props.hasOwnProperty('userRef') ? props.userRef : nodeUserRef ?? ownerOperationRef; setOwner = _setOwner; @@ -510,26 +492,16 @@ describe('usePaginationFragment', () => { setEnvironment = _setEnv; - return ( - - {children} - - ); + return {children}; }; - renderFragment = (args?: { - isConcurrent?: boolean; - owner?: any; - userRef?: any; - fragment?: any; - }): any => { + renderFragment = (args?: { isConcurrent?: boolean; owner?: any; userRef?: any; fragment?: any }): any => { const { isConcurrent = false, ...props } = args ?? {}; let renderer; TestRenderer.act(() => { renderer = TestRenderer.create( { - console.log('error', error); return `Error: ${error.message}`; }} > @@ -589,9 +561,7 @@ describe('usePaginationFragment', () => { } `); const renderer = renderFragment({ fragment: UserFragment }); - expect( - renderer.toJSON().includes('Remove `@relay(plural: true)` from fragment'), - ).toEqual(true); + expect(renderer.toJSON().includes('Remove `@relay(plural: true)` from fragment')).toEqual(true); }); it('should throw error if fragment is missing @refetchable directive', () => { @@ -615,9 +585,7 @@ describe('usePaginationFragment', () => { }); const renderer = renderFragment({ fragment: UserFragment, owner }); expect( - renderer - .toJSON() - .includes('Did you forget to add a @refetchable directive to the fragment?'), + renderer.toJSON().includes('Did you forget to add a @refetchable directive to the fragment?'), ).toEqual(true); }); @@ -634,7 +602,7 @@ describe('usePaginationFragment', () => { const UserFragment = getFragment(graphql` fragment usePaginationFragmentTest3UserFragment on User - @refetchable(queryName: "usePaginationFragmentTest3UserFragmentRefetchQuery") { + @refetchable(queryName: "usePaginationFragmentTest3UserFragmentRefetchQuery") { id } `); @@ -643,14 +611,10 @@ describe('usePaginationFragment', () => { force: true, }); const renderer = renderFragment({ fragment: UserFragment, owner }); - console.log('renderer.toJSON()', renderer.toJSON()); - console.log('UserFragment', UserFragment); expect( renderer .toJSON() - .includes( - 'Did you forget to add a @connection directive to the connection field in the fragment?', - ), + .includes('Did you forget to add a @connection directive to the connection field in the fragment?'), ).toEqual(true); }); @@ -882,9 +846,7 @@ describe('usePaginationFragment', () => { expect(warning).toHaveBeenCalledTimes(1); expect( - (warning as any).mock.calls[0][1].includes( - 'Relay: Unexpected fetch on unmounted component', - ), + (warning as any).mock.calls[0][1].includes('Relay: Unexpected fetch on unmounted component'), ).toEqual(true); expect(environment.execute).toHaveBeenCalledTimes(0); }); @@ -1606,8 +1568,7 @@ describe('usePaginationFragment', () => { }); // Get fragment ref for user using nested fragment - const userRef = (environment.lookup(queryNestedFragment.fragment).data as any)?.node - ?.actor; + const userRef = (environment.lookup(queryNestedFragment.fragment).data as any)?.node?.actor; initialUser = { id: '1', @@ -3320,9 +3281,7 @@ describe('usePaginationFragment', () => { // Assert query is tentatively retained while component is suspended expect(environment.retain).toBeCalledTimes(1); - expect(environment.retain.mock.calls[0][0]).toEqual( - expected.refetchQuery ?? paginationQuery, - ); + expect(environment.retain.mock.calls[0][0]).toEqual(expected.refetchQuery ?? paginationQuery); } it('refetches new variables correctly when refetching new id', () => { @@ -3426,13 +3385,6 @@ describe('usePaginationFragment', () => { hasNext: true, hasPrevious: false, }, - { - data: expectedUser, - isLoadingNext: false, - isLoadingPrevious: false, - hasNext: true, - hasPrevious: false, - }, ]); // Assert refetch query was retained @@ -3542,13 +3494,6 @@ describe('usePaginationFragment', () => { hasNext: true, hasPrevious: false, }, - { - data: expectedUser, - isLoadingNext: false, - isLoadingPrevious: false, - hasNext: true, - hasPrevious: false, - }, ]); // Assert refetch query was retained @@ -3591,8 +3536,7 @@ describe('usePaginationFragment', () => { }); // Get fragment ref for user using nested fragment - const userRef = (environment.lookup(queryNestedFragment.fragment).data as any)?.node - ?.actor; + const userRef = (environment.lookup(queryNestedFragment.fragment).data as any)?.node?.actor; initialUser = { id: '1', @@ -3720,13 +3664,6 @@ describe('usePaginationFragment', () => { hasNext: true, hasPrevious: false, }, - { - data: expectedUser, - isLoadingNext: false, - isLoadingPrevious: false, - hasNext: true, - hasPrevious: false, - }, ]); // Assert refetch query was retained @@ -3836,13 +3773,6 @@ describe('usePaginationFragment', () => { hasNext: true, hasPrevious: false, }, - { - data: expectedUser, - isLoadingNext: false, - isLoadingPrevious: false, - hasNext: true, - hasPrevious: false, - }, ]); // Assert refetch query was retained @@ -3969,15 +3899,9 @@ describe('usePaginationFragment', () => { gqlFragment = getFragment(graphql` fragment usePaginationFragmentTestStoryFragment on NonNodeStory - @argumentDefinitions( - count: { type: "Int", defaultValue: 10 } - cursor: { type: "ID" } - ) - @refetchable( - queryName: "usePaginationFragmentTestStoryFragmentRefetchQuery" - ) { - comments(first: $count, after: $cursor) - @connection(key: "StoryFragment_comments") { + @argumentDefinitions(count: { type: "Int", defaultValue: 10 }, cursor: { type: "ID" }) + @refetchable(queryName: "usePaginationFragmentTestStoryFragmentRefetchQuery") { + comments(first: $count, after: $cursor) @connection(key: "StoryFragment_comments") { edges { node { id @@ -3986,8 +3910,8 @@ describe('usePaginationFragment', () => { } } `); - gqlPaginationQuery = require('./__generated__/usePaginationFragmentTestStoryFragmentRefetchQuery.graphql') - .default; + gqlPaginationQuery = + require('./__generated__/usePaginationFragmentTestStoryFragmentRefetchQuery.graphql').default; const fetchVariables = { id: 'a' }; //gqlRefetchQuery = generated.StoryFragmentRefetchQuery; invariant( diff --git a/__tests__/useRefetchable-test.tsx b/__tests__/useRefetchable-test.tsx index 3deadf7e..7e3f720b 100644 --- a/__tests__/useRefetchable-test.tsx +++ b/__tests__/useRefetchable-test.tsx @@ -39,13 +39,7 @@ const ReactRelayRefetchContainer = { }); }; return ( - + ); }, }; @@ -133,8 +127,8 @@ describe('useRefetchable', () => { UserFragment = graphql` fragment useRefetchableTestUserUserFragment on User - @refetchable(queryName: "useRefetchableTestUserUserFragmentRefetchQuery") - @argumentDefinitions(cond: { type: "Boolean", defaultValue: true }) { + @refetchable(queryName: "useRefetchableTestUserUserFragmentRefetchQuery") + @argumentDefinitions(cond: { type: "Boolean", defaultValue: true }) { id name @include(if: $cond) } @@ -154,11 +148,7 @@ describe('useRefetchable', () => { variables = {}; TestComponent = render; TestComponent.displayName = 'TestComponent'; - TestContainer = ReactRelayRefetchContainer.createContainer( - TestComponent, - UserFragment, - UserQuery, - ); + TestContainer = ReactRelayRefetchContainer.createContainer(TestComponent, UserFragment, UserQuery); // Pre-populate the store with data ownerUser1 = createOperationDescriptor(UserQuery, { id: '4' }); @@ -367,12 +357,7 @@ describe('useRefetchable', () => { missingClientEdges: null, missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '842472', - { cond: true }, - ownerUser2.request, - ), + selector: createReaderSelector(UserFragment, '842472', { cond: true }, ownerUser2.request), }); }); @@ -387,8 +372,7 @@ describe('useRefetchable', () => { environment.lookup.mockClear(); environment.subscribe.mockClear(); - userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data - .node; + userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data.node; instance.getInstance().setProps({ user: userPointer, }); @@ -421,12 +405,7 @@ describe('useRefetchable', () => { relayResolverErrors: [], missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '4', - { cond: false }, - ownerUser1WithCondVar.request, - ), + selector: createReaderSelector(UserFragment, '4', { cond: false }, ownerUser1WithCondVar.request), }); }); @@ -447,9 +426,7 @@ describe('useRefetchable', () => { id: '4', }; refetch(refetchVariables, null, jest.fn()); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache), - ).toBe(true); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache)).toBe(true); environment.mock.resolve(UserFragmentRefetchQuery, { data: { @@ -463,8 +440,7 @@ describe('useRefetchable', () => { environment.subscribe.mockClear(); // Pass an updated user pointer that references different variables - userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data - .node; + userPointer = environment.lookup(ownerUser1WithCondVar.fragment, ownerUser1WithCondVar).data.node; instance.getInstance().setProps({ user: userPointer, }); @@ -497,12 +473,7 @@ describe('useRefetchable', () => { missingClientEdges: null, missingRequiredFields: null, seenRecords: expect.any(Object), - selector: createReaderSelector( - UserFragment, - '4', - { cond: false }, - ownerUser1WithCondVar.request, - ), + selector: createReaderSelector(UserFragment, '4', { cond: false }, ownerUser1WithCondVar.request), }); }); @@ -674,9 +645,7 @@ describe('useRefetchable', () => { id: '4', }; refetch(refetchVariables, null, jest.fn()); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache), - ).toBe(true); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache)).toBe(true); environment.mock.resolve(UserFragmentRefetchQuery, { data: { node: { @@ -688,7 +657,7 @@ describe('useRefetchable', () => { }); it('reads data from the store without sending a network request when data is available in store and using store-or-network', () => { - expect.assertions(3); + expect.assertions(4); const refetchVariables = { cond: false, id: '4', @@ -696,11 +665,10 @@ describe('useRefetchable', () => { const refetchOptions = { fetchPolicy: 'store-or-network', }; + expect(render.mock.calls.length).toBe(1); refetch(refetchVariables, null, jest.fn(), refetchOptions); expect(render.mock.calls.length).toBe(2); - expect( - environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache), - ).toBe(false); + expect(environment.mock.isLoading(UserFragmentRefetchQuery, refetchVariables, forceCache)).toBe(false); expect(environment.execute).toBeCalledTimes(0); }); @@ -813,7 +781,7 @@ describe('useRefetchable', () => { }); it('renders with the results of the new variables on success', () => { - expect.assertions(10); + expect.assertions(8); expect(render.mock.calls.length).toBe(1); expect(render.mock.calls[0][0].user.name).toBe('Zuck'); variables = { @@ -833,11 +801,9 @@ describe('useRefetchable', () => { }, }, }); - expect(render.mock.calls.length).toBe(4); - expect(render.mock.calls[2][0].isLoading).toBe(true); + expect(render.mock.calls.length).toBe(3); + expect(render.mock.calls[2][0].isLoading).toBe(false); expect(render.mock.calls[2][0].user.name).toBe(undefined); - expect(render.mock.calls[3][0].isLoading).toBe(false); - expect(render.mock.calls[3][0].user.name).toBe(undefined); }); it('does not update variables on failure', () => { @@ -950,7 +916,7 @@ describe('useRefetchable', () => { }); ReactTestRenderer.act(() => { instance.unmount(); - }) + }); expect(references.length).toBe(1); expect(references[0].dispose).toBeCalled(); }); diff --git a/__tests__/useRefetchableFragment-test.tsx b/__tests__/useRefetchableFragment-test.tsx index 01a1e89c..55ae5054 100644 --- a/__tests__/useRefetchableFragment-test.tsx +++ b/__tests__/useRefetchableFragment-test.tsx @@ -83,12 +83,7 @@ const invariant = require('fbjs/lib/invariant'); const warning = require('fbjs/lib/warning'); const areEqual = require('fbjs/lib/areEqual'); -const { - FRAGMENT_OWNER_KEY, - FRAGMENTS_KEY, - ID_KEY, - createOperationDescriptor, -} = require('relay-runtime'); +const { FRAGMENT_OWNER_KEY, FRAGMENTS_KEY, ID_KEY, createOperationDescriptor } = require('relay-runtime'); describe('useRefetchableFragmentNode', () => { let environment; @@ -136,10 +131,7 @@ describe('useRefetchableFragmentNode', () => { } function useRefetchableFragmentNode(fragmentNode, fragmentRef) { - const { data, refetch: refetchFunction } = useRefetchableFragmentNodeOriginal( - fragmentNode, - fragmentRef, - ); + const { data, refetch: refetchFunction } = useRefetchableFragmentNodeOriginal(fragmentNode, fragmentRef); refetch = refetchFunction; renderSpy(data, refetch); return data; @@ -195,10 +187,8 @@ describe('useRefetchableFragmentNode', () => { `; gqlFragmentWithArgs = getFragment(graphql` fragment useRefetchableFragmentTestUserFragmentWithArgs on User - @refetchable( - queryName: "useRefetchableFragmentTestUserFragmentWithArgsRefetchQuery" - ) - @argumentDefinitions(scaleLocal: { type: "Float!" }) { + @refetchable(queryName: "useRefetchableFragmentTestUserFragmentWithArgsRefetchQuery") + @argumentDefinitions(scaleLocal: { type: "Float!" }) { id name profile_picture(scale: $scaleLocal) { @@ -209,7 +199,7 @@ describe('useRefetchableFragmentNode', () => { `); gqlFragment = getFragment(graphql` fragment useRefetchableFragmentTestUserFragment on User - @refetchable(queryName: "useRefetchableFragmentTestUserFragmentRefetchQuery") { + @refetchable(queryName: "useRefetchableFragmentTestUserFragmentRefetchQuery") { id name profile_picture(scale: $scale) { @@ -248,10 +238,9 @@ describe('useRefetchableFragmentNode', () => { } } `); - gqlRefetchQuery = require('./__generated__/useRefetchableFragmentTestUserFragmentRefetchQuery.graphql') - .default; - gqlRefetchQueryWithArgs = require('./__generated__/useRefetchableFragmentTestUserFragmentWithArgsRefetchQuery.graphql') - .default; + gqlRefetchQuery = require('./__generated__/useRefetchableFragmentTestUserFragmentRefetchQuery.graphql').default; + gqlRefetchQueryWithArgs = + require('./__generated__/useRefetchableFragmentTestUserFragmentWithArgsRefetchQuery.graphql').default; variables = { id: '1', scale: 16 }; variablesNestedFragment = { id: '', scale: 16 }; @@ -260,19 +249,14 @@ describe('useRefetchableFragmentNode', () => { 'useRefetchableFragment-test: Expected refetchable fragment metadata to contain operation.', ); invariant( - areEqual( - gqlFragmentWithArgs.metadata?.refetch?.operation.default, - gqlRefetchQueryWithArgs, - ), + areEqual(gqlFragmentWithArgs.metadata?.refetch?.operation.default, gqlRefetchQueryWithArgs), 'useRefetchableFragment-test: Expected refetchable fragment metadata to contain operation.', ); query = createOperationDescriptor(gqlQuery, variables, { force: true }); - queryNestedFragment = createOperationDescriptor( - gqlQueryNestedFragment, - variablesNestedFragment, - { force: true }, - ); + queryNestedFragment = createOperationDescriptor(gqlQueryNestedFragment, variablesNestedFragment, { + force: true, + }); refetchQuery = createOperationDescriptor(gqlRefetchQuery, variables, { force: true, }); @@ -332,11 +316,7 @@ describe('useRefetchableFragmentNode', () => { setEnvironment = _setEnv; - return ( - - {children} - - ); + return {children}; }; const Fallback = (): any => { @@ -347,24 +327,24 @@ describe('useRefetchableFragmentNode', () => { return 'Fallback'; }; - renderFragment = (args?: { - isConcurrent?: boolean; - owner?: any; - userRef?: any; - fragment?: any; - }): any => { + renderFragment = (args?: { isConcurrent?: boolean; owner?: any; userRef?: any; fragment?: any }): any => { const { isConcurrent = false, ...props } = args ?? ({} as any); - return TestRenderer.create( - `Error: ${error.message}`}> - }> - - - - - , - // any[prop-missing] - error revealed when flow-typing ReactTestRenderer - { unstable_isConcurrent: isConcurrent } as any, - ); + let renderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + `Error: ${error.message}`}> + }> + + + + + , + // any[prop-missing] - error revealed when flow-typing ReactTestRenderer + { unstable_isConcurrent: isConcurrent } as any, + ); + jest.runAllImmediates(); + }); + return renderer; }; }); @@ -388,9 +368,7 @@ describe('useRefetchableFragmentNode', () => { } `); const renderer = renderFragment({ fragment: UserFragment }); - expect( - renderer.toJSON().includes('Remove `@relay(plural: true)` from fragment'), - ).toEqual(true); + expect(renderer.toJSON().includes('Remove `@relay(plural: true)` from fragment')).toEqual(true); }); it('should throw error if fragment is missing @refetchable directive', () => { @@ -403,9 +381,7 @@ describe('useRefetchableFragmentNode', () => { `); const renderer = renderFragment({ fragment: UserFragment }); expect( - renderer - .toJSON() - .includes('Did you forget to add a @refetchable directive to the fragment?'), + renderer.toJSON().includes('Did you forget to add a @refetchable directive to the fragment?'), ).toEqual(true); }); @@ -568,9 +544,7 @@ describe('useRefetchableFragmentNode', () => { expect(warning).toHaveBeenCalledTimes(1); expect( // any[prop-missing] - warning.mock.calls[0][1].includes( - 'Relay: Unexpected call to `refetch` on unmounted component', - ), + warning.mock.calls[0][1].includes('Relay: Unexpected call to `refetch` on unmounted component'), ).toEqual(true); expect(environment.execute).toHaveBeenCalledTimes(0); }); @@ -816,8 +790,7 @@ describe('useRefetchableFragmentNode', () => { }); // Get fragment ref for user using nested fragment - const userRef = (environment.lookup(queryNestedFragment.fragment).data as any)?.node - ?.actor; + const userRef = (environment.lookup(queryNestedFragment.fragment).data as any)?.node?.actor; const renderer = renderFragment({ owner: queryNestedFragment, userRef }); const initialUser = { @@ -907,11 +880,9 @@ describe('useRefetchableFragmentNode', () => { id: '1', scaleLocal: 32, }; - refetchQueryWithArgs = createOperationDescriptor( - gqlRefetchQueryWithArgs, - refetchVariables, - { force: true }, - ); + refetchQueryWithArgs = createOperationDescriptor(gqlRefetchQueryWithArgs, refetchVariables, { + force: true, + }); expectFragmentIsRefetching(renderer, { refetchVariables, refetchQuery: refetchQueryWithArgs, @@ -977,11 +948,9 @@ describe('useRefetchableFragmentNode', () => { id: '4', scaleLocal: 16, }; - refetchQueryWithArgs = createOperationDescriptor( - gqlRefetchQueryWithArgs, - refetchVariables, - { force: true }, - ); + refetchQueryWithArgs = createOperationDescriptor(gqlRefetchQueryWithArgs, refetchVariables, { + force: true, + }); expectFragmentIsRefetching(renderer, { refetchVariables, refetchQuery: refetchQueryWithArgs, @@ -2017,19 +1986,12 @@ describe('useRefetchableFragmentNode', () => { renderFragment(); renderSpy.mockClear(); TestRenderer.act(() => { - refetch( - { id: '1' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '1' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert request is not started const refetchVariables = { ...variables }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectRequestIsInFlight({ inFlight: false, requestCount: 0, @@ -2045,7 +2007,7 @@ describe('useRefetchableFragmentNode', () => { ...createFragmentRef('1', query), //...createFragmentRef('1', refetchQuery), //original relay }; - // expectFragmentResults([{ data: refetchedUser }, { data: refetchedUser }]); original relay + expectFragmentResults([{ data: refetchedUser }]); //original relay }); it('starts network request if refetch query is not fully cached and suspends if fragment has missing data', () => { @@ -2059,10 +2021,7 @@ describe('useRefetchableFragmentNode', () => { expectFragmentResults([{ data: initialUser }]); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert that fragment is refetching with the right variables and @@ -2071,11 +2030,7 @@ describe('useRefetchableFragmentNode', () => { id: '4', scale: 16, }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectFragmentIsRefetching(renderer, { refetchVariables, refetchQuery, @@ -2114,11 +2069,7 @@ describe('useRefetchableFragmentNode', () => { it("starts network request if refetch query is not fully cached and doesn't suspend if fragment doesn't have missing data", () => { // Cache user with missing username const refetchVariables = { id: '4', scale: 16 }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); environment.commitPayload(refetchQuery, { node: { __typename: 'User', @@ -2131,10 +2082,7 @@ describe('useRefetchableFragmentNode', () => { renderFragment(); renderSpy.mockClear(); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert request is started @@ -2165,19 +2113,12 @@ describe('useRefetchableFragmentNode', () => { renderFragment(); renderSpy.mockClear(); TestRenderer.act(() => { - refetch( - { id: '1' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '1' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert request is not started const refetchVariables = { ...variables }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectRequestIsInFlight({ inFlight: false, requestCount: 0, @@ -2193,7 +2134,7 @@ describe('useRefetchableFragmentNode', () => { ...createFragmentRef('1', query), //...createFragmentRef('1', refetchQuery), //original relay }; - // expectFragmentResults([{ data: refetchedUser }, { data: refetchedUser }]); original relay + expectFragmentResults([{ data: refetchedUser }]); //original relay }); it('starts network request if refetch query is not fully cached and suspends if fragment has missing data', () => { @@ -2207,10 +2148,7 @@ describe('useRefetchableFragmentNode', () => { expectFragmentResults([{ data: initialUser }]); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert that fragment is refetching with the right variables and @@ -2219,11 +2157,7 @@ describe('useRefetchableFragmentNode', () => { id: '4', scale: 16, }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectFragmentIsRefetching(renderer, { refetchVariables, refetchQuery, @@ -2262,11 +2196,7 @@ describe('useRefetchableFragmentNode', () => { it("starts network request if refetch query is not fully cached and suspends even if fragment doesn't have missing data", () => { // Cache user with missing username const refetchVariables = { id: '4', scale: 16 }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); environment.commitPayload(refetchQuery, { node: { __typename: 'User', @@ -2286,10 +2216,7 @@ describe('useRefetchableFragmentNode', () => { expectFragmentResults([{ data: initialUser }]); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); expectFragmentIsRefetching(renderer, { @@ -2343,19 +2270,12 @@ describe('useRefetchableFragmentNode', () => { renderFragment(); renderSpy.mockClear(); TestRenderer.act(() => { - refetch( - { id: '1' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '1' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert request is not started const refetchVariables = { ...variables }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectRequestIsInFlight({ inFlight: true, requestCount: 1, @@ -2386,10 +2306,7 @@ describe('useRefetchableFragmentNode', () => { expectFragmentResults([{ data: initialUser }]); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert that fragment is refetching with the right variables and @@ -2398,11 +2315,7 @@ describe('useRefetchableFragmentNode', () => { id: '4', scale: 16, }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectFragmentIsRefetching(renderer, { refetchVariables, refetchQuery, @@ -2440,11 +2353,7 @@ describe('useRefetchableFragmentNode', () => { it("starts network request if refetch query is not fully cached and doesn't suspend if fragment doesn't have missing data", () => { // Cache user with missing username const refetchVariables = { id: '4', scale: 16 }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); environment.commitPayload(refetchQuery, { node: { __typename: 'User', @@ -2457,10 +2366,7 @@ describe('useRefetchableFragmentNode', () => { renderFragment(); renderSpy.mockClear(); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert request is started @@ -2492,19 +2398,12 @@ describe('useRefetchableFragmentNode', () => { renderFragment(); renderSpy.mockClear(); TestRenderer.act(() => { - refetch( - { id: '1' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '1' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert request is not started const refetchVariables = { ...variables }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectRequestIsInFlight({ inFlight: true, requestCount: 1, @@ -2535,10 +2434,7 @@ describe('useRefetchableFragmentNode', () => { expectFragmentResults([{ data: initialUser }]); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert that fragment is refetching with the right variables and @@ -2547,11 +2443,7 @@ describe('useRefetchableFragmentNode', () => { id: '4', scale: 16, }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); expectFragmentIsRefetching(renderer, { refetchVariables, refetchQuery, @@ -2590,11 +2482,7 @@ describe('useRefetchableFragmentNode', () => { it("starts network request if refetch query is not fully cached and doesn't suspend if fragment doesn't have missing data", () => { // Cache user with missing username const refetchVariables = { id: '4', scale: 16 }; - refetchQuery = createOperationDescriptor( - gqlRefetchQuery, - refetchVariables, - { force: true }, - ); + refetchQuery = createOperationDescriptor(gqlRefetchQuery, refetchVariables, { force: true }); environment.commitPayload(refetchQuery, { node: { __typename: 'User', @@ -2607,10 +2495,7 @@ describe('useRefetchableFragmentNode', () => { const renderer = renderFragment(); renderSpy.mockClear(); TestRenderer.act(() => { - refetch( - { id: '4' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + refetch({ id: '4' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert component suspended @@ -2799,7 +2684,7 @@ describe('useRefetchableFragmentNode', () => { ...createFragmentRef('1', query), //...createFragmentRef('1', refetchQuery), //original relay }; - // expectFragmentResults([{ data: refetchedUser }, { data: refetchedUser }]); original relay + expectFragmentResults([{ data: refetchingUser }]); // original relay }); it("doesn't start network request if refetch query is not fully cached", () => { @@ -3064,10 +2949,7 @@ describe('useRefetchableFragmentNode', () => { renderSpy.mockClear(); let disposable; TestRenderer.act(() => { - disposable = refetch( - { id: '1' }, - { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }, - ); + disposable = refetch({ id: '1' }, { fetchPolicy, UNSTABLE_renderPolicy: renderPolicy }); }); // Assert request is started @@ -3111,7 +2993,7 @@ describe('useRefetchableFragmentNode', () => { beforeEach(() => { gqlFragment = getFragment(graphql` fragment useRefetchableFragmentTest1Fragment on NonNodeStory - @refetchable(queryName: "useRefetchableFragmentTest1FragmentRefetchQuery") { + @refetchable(queryName: "useRefetchableFragmentTest1FragmentRefetchQuery") { actor { name } @@ -3127,8 +3009,8 @@ describe('useRefetchableFragmentNode', () => { `); variables = { id: 'a' }; - gqlRefetchQuery = require('./__generated__/useRefetchableFragmentTest1FragmentRefetchQuery.graphql') - .default; + gqlRefetchQuery = + require('./__generated__/useRefetchableFragmentTest1FragmentRefetchQuery.graphql').default; invariant( areEqual(gqlFragment.metadata?.refetch?.operation.default, gqlRefetchQuery), 'useRefetchableFragment-test: Expected refetchable fragment metadata to contain operation.', @@ -3357,7 +3239,7 @@ describe('useRefetchableFragmentNode', () => { `; gqlFragment = getFragment(graphql` fragment useRefetchableFragmentTest3Fragment on User - @refetchable(queryName: "useRefetchableFragmentTest3FragmentRefetchQuery") { + @refetchable(queryName: "useRefetchableFragmentTest3FragmentRefetchQuery") { id name profile_picture(scale: $scale) { @@ -3373,8 +3255,8 @@ describe('useRefetchableFragmentNode', () => { } } `); - gqlRefetchQuery = require('./__generated__/useRefetchableFragmentTest3FragmentRefetchQuery.graphql') - .default; + gqlRefetchQuery = + require('./__generated__/useRefetchableFragmentTest3FragmentRefetchQuery.graphql').default; variables = { nodeID: '1', scale: 16 }; invariant( areEqual(gqlFragment.metadata?.refetch?.operation.default, gqlRefetchQuery), @@ -3453,12 +3335,7 @@ describe('useRefetchableFragmentNode', () => { profile_picture: { uri: 'scale16', }, - ...createFragmentRef( - '4', - refetchQuery, - false, - 'useRefetchableFragmentTest2Fragment', - ), + ...createFragmentRef('4', refetchQuery, false, 'useRefetchableFragmentTest2Fragment'), }; // expectFragmentResults([{ data: refetchedUser }, { data: refetchedUser }]); original relay expectFragmentResults([{ data: refetchedUser }]); @@ -3525,12 +3402,7 @@ describe('useRefetchableFragmentNode', () => { profile_picture: { uri: 'scale32', }, - ...createFragmentRef( - '1', - refetchQuery, - false, - 'useRefetchableFragmentTest2Fragment', - ), + ...createFragmentRef('1', refetchQuery, false, 'useRefetchableFragmentTest2Fragment'), }; // expectFragmentResults([{ data: refetchedUser }, { data: refetchedUser }]); original relay expectFragmentResults([{ data: refetchedUser }]); diff --git a/examples/relay-hook-example/nextjs-ssr-preload/components/Header.tsx b/examples/relay-hook-example/nextjs-ssr-preload/components/Header.tsx index 3d054399..0a8d6cd1 100644 --- a/examples/relay-hook-example/nextjs-ssr-preload/components/Header.tsx +++ b/examples/relay-hook-example/nextjs-ssr-preload/components/Header.tsx @@ -1,49 +1,47 @@ import React from 'react'; import Link from 'next/link'; -import styled, {css} from 'styled-components'; -import {withRouter} from 'next/router'; +import styled, { css } from 'styled-components'; +import { withRouter } from 'next/router'; const StyledButton = styled.button` - margin: auto; - padding: 10px; - cursor: pointer; - display: -webkit-box; - flex: 1; - ${props => - props.selected && - css` - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); - box-sizing: border-box; - `} + margin: auto; + padding: 10px; + cursor: pointer; + display: -webkit-box; + flex: 1; + ${(props) => + props.selected && + css` + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + `} `; -const MyButton = React.forwardRef( - ({onClick, href, children, selected}: any, ref) => ( +const MyButton = React.forwardRef(({ onClick, href, children, selected }: any, ref) => ( - {children} + {children} - ), -); +)); const StyledDiv = styled.div` - display: flex; - background: #fff; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); + display: flex; + background: #fff; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); `; -const Header = ({userId}) => { - const selectedYou = userId === 'you'; - return ( - - - ME - - - YOU - - - ); +const Header = ({ userId }) => { + const selectedYou = userId === 'you'; + return ( + + + ME + + + YOU + + + ); }; export default Header; diff --git a/examples/relay-hook-example/nextjs-ssr-preload/components/Home.tsx b/examples/relay-hook-example/nextjs-ssr-preload/components/Home.tsx index 638ef60a..8cb84c71 100644 --- a/examples/relay-hook-example/nextjs-ssr-preload/components/Home.tsx +++ b/examples/relay-hook-example/nextjs-ssr-preload/components/Home.tsx @@ -1,19 +1,16 @@ import React from 'react'; -import TodoApp, {QUERY_APP} from './TodoApp'; -import {usePreloadedQuery, useQuery} from 'relay-hooks'; -import {TodoAppQuery} from '../__generated__/relay/TodoAppQuery.graphql'; +import TodoApp, { QUERY_APP } from './TodoApp'; +import { usePreloadedQuery, useQuery } from 'relay-hooks'; +import { TodoAppQuery } from '../__generated__/relay/TodoAppQuery.graphql'; -const Home = ({prefetch}) => { - console.log('prefetch ssr', prefetch); - const {error, cached, props, retry} = usePreloadedQuery( - prefetch, - ); - if (props) { - return ; - } else if (error) { - return
{error.message}
; - } - return
loading
; +const Home = ({ prefetch }) => { + const { error, data, retry } = usePreloadedQuery(prefetch); + if (data) { + return ; + } else if (error) { + return
{error.message}
; + } + return
loading
; }; export default Home; diff --git a/examples/relay-hook-example/nextjs-ssr-preload/components/TodoApp.tsx b/examples/relay-hook-example/nextjs-ssr-preload/components/TodoApp.tsx index 2f52ac8d..ce84a23d 100644 --- a/examples/relay-hook-example/nextjs-ssr-preload/components/TodoApp.tsx +++ b/examples/relay-hook-example/nextjs-ssr-preload/components/TodoApp.tsx @@ -1,137 +1,144 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import AddTodoMutation from '../mutations/AddTodoMutation'; import TodoList from './TodoList'; import TodoListFooter from './TodoListFooter'; import TodoTextInput from './TodoTextInput'; import styled from 'styled-components'; import Header from './Header'; -import {useRefetch, useRelayEnvironment, graphql} from 'relay-hooks'; -import {TodoApp_user$key} from '../__generated__/relay/TodoApp_user.graphql'; +import { useRefetchable, useRelayEnvironment, graphql, useFragment } from 'relay-hooks'; +import { TodoApp_user$key } from '../__generated__/relay/TodoApp_user.graphql'; +import { useRouter } from 'next/router'; //import {TodoApp_user$key} from 'relay/TodoApp_user.graphql'; //import TodoApp, { fragmentSpec } from './components/TodoApp'; export const QUERY_APP = graphql` - query TodoAppQuery($userId: String) { - ...TodoApp_user - } + query TodoAppQuery($userId: String) { + ...TodoApp_user + } `; const StyledSection = styled.section` - flex: 1; - background: #fff; - margin: 130px 0 40px 0; - position: relative; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); - h1 { - position: absolute; - top: -155px; - width: 100%; - font-size: 100px; - font-weight: 100; - text-align: center; - color: rgba(175, 47, 47, 0.15); - -webkit-text-rendering: optimizeLegibility; - -moz-text-rendering: optimizeLegibility; - text-rendering: optimizeLegibility; - } + flex: 1; + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); + h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; + } `; const StyledButton = styled.button` - margin: auto; - padding: 10px; - cursor: pointer; - display: -webkit-box; + margin: auto; + padding: 10px; + cursor: pointer; + display: -webkit-box; `; const StyledFooter = styled.footer` - margin: 65px auto 0; - color: #bfbfbf; - font-size: 10px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-align: center; + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; `; const StyledP = styled.p` - line-height: 1; + line-height: 1; `; const StyledDivButton = styled.div` - display: flex; - background: #fff; + display: flex; + background: #fff; `; const isServer = typeof window === 'undefined'; type Props = { - query: TodoApp_user$key; - retry: () => void; + query: TodoApp_user$key; + retry: () => void; }; const fragmentSpec = graphql` - fragment TodoApp_user on Query { - user(id: $userId) { - id - userId - totalCount - completedCount - ...TodoListFooter_user - ...TodoList_user + fragment TodoApp_user on Query @refetchable(queryName: "TodoAppRefetchTableQuery") { + user(id: $userId) { + id + userId + totalCount + completedCount + ...TodoListFooter_user + ...TodoList_user + } } - } `; export function isNotNull(it: T): it is NonNullable { - return it != null; + return it != null; } const AppTodo = (props: Props) => { - const environment = useRelayEnvironment(); - const [{user}, refetch] = useRefetch(fragmentSpec, props.query); - - if (!user) { - return
; - } - - const handleTextInputSave = (text: string) => { - AddTodoMutation.commit(environment, text, user); - return; - }; - - const hasTodos = user.totalCount > 0; - console.log('renderer'); - return ( - -
- -
-

todos

- - -
- - - {hasTodos && } - - Retry - - - { - refetch(QUERY_APP, { - userId: user.userId === 'me' ? 'you' : 'me', - }); - }}> - Change User - - - - Double-click to edit a todo - -
- - ); + const environment = useRelayEnvironment(); + const { + data: { user }, + refetch, + } = useRefetchable(fragmentSpec, props.query); + + const router = useRouter(); + + const { userId } = user || {}; + + const changeUser = useCallback(() => { + const pathname = userId === 'me' ? '/you' : '/'; + router.push({ + pathname, + }); + }, [router, userId]); + + if (!user) { + return
; + } + + const handleTextInputSave = (text: string) => { + AddTodoMutation.commit(environment, text, user); + return; + }; + + const hasTodos = user.totalCount > 0; + console.log('renderer'); + return ( + +
+ +
+

todos

+ + +
+ + + {hasTodos && } + + Retry + + + Change User + + + Refetch + + + Double-click to edit a todo + +
+ + ); }; export default AppTodo; diff --git a/examples/relay-hook-example/nextjs-ssr-preload/package.json b/examples/relay-hook-example/nextjs-ssr-preload/package.json index 364f09d6..57c72932 100644 --- a/examples/relay-hook-example/nextjs-ssr-preload/package.json +++ b/examples/relay-hook-example/nextjs-ssr-preload/package.json @@ -4,10 +4,17 @@ "dev": "babel-node server.js", "build": "next build", "start": "cross-env NODE_ENV=production babel-node server.js", - "compile": "relay-compiler --src ./ --include '**/pages/**' '**/components/**' '**/mutations/**' --schema ./data/schema.graphql --language typescript --artifactDirectory ./__generated__/relay", + "compile": "relay-compiler", "update-schema": "babel-node ./scripts/updateSchema.js", "lint": "eslint ./ --cache" }, + "relay": { + "src": "./", + "schema": "./data/schema.graphql", + "language": "typescript", + "artifactDirectory": "./__generated__/relay", + "excludes": ["__generated__", "node_modules/**", ".next"] + }, "dependencies": { "classnames": "2.2.6", "es6-promise": "4.2.8", @@ -16,12 +23,13 @@ "graphql": "^14.5.8", "graphql-relay": "^0.6.0", "isomorphic-unfetch": "3.0.0", + "isomorphic-fetch": "^2.1.1", "next": "9.1.1", "prop-types": "^15.7.2", "react": "^16.9.0", "react-dom": "^16.9.0", - "relay-hooks": "3.7.0", - "relay-runtime": "9.0.0", + "relay-hooks": "../../../relay-hooks-7.3.0-a5.tgz", + "relay-runtime": "^14.0.0", "styled-components": "4.4.0", "whatwg-fetch": "3.0.0" }, @@ -36,12 +44,11 @@ "@babel/runtime": "^7.3.4", "@types/node": "^12.7.12", "@types/react": "^16.8.15", - "@types/react-relay": "^7.0.2", - "@types/relay-runtime": "^6.0.11", + "@types/relay-runtime": "^14.1.10", "@zeit/next-typescript": "1.1.1", "babel-eslint": "^10.0.1", "babel-loader": "^8.0.5", - "babel-plugin-relay": "^9.0.0", + "babel-plugin-relay": "^14.0.0", "cross-env": "6.0.3", "eslint": "^5.13.0", "eslint-config-fbjs": "^2.1.0", @@ -55,7 +62,7 @@ "flow-bin": "^0.94.0", "flow-typed": "^2.5.1", "prettier": "^1.16.4", - "relay-compiler": "^9.0.0", + "relay-compiler": "^14.0.0", "relay-compiler-language-typescript": "12.0.0", "typescript": "3.7.3" }, diff --git a/examples/relay-hook-example/nextjs-ssr-preload/pages/_app.tsx b/examples/relay-hook-example/nextjs-ssr-preload/pages/_app.tsx index 017667df..da6a6c21 100644 --- a/examples/relay-hook-example/nextjs-ssr-preload/pages/_app.tsx +++ b/examples/relay-hook-example/nextjs-ssr-preload/pages/_app.tsx @@ -2,99 +2,96 @@ import React from 'react'; import App from 'next/app'; import Head from 'next/head'; import Header from '../components/Header'; -import {useEffect} from 'react'; -import {Router} from 'next/router'; +import { useEffect } from 'react'; +import { Router } from 'next/router'; import initEnvironment from '../relay/createRelayEnvironment'; -import {QUERY_APP} from '../components/TodoApp'; -import {RelayEnvironmentProvider, useRelayEnvironment} from 'relay-hooks'; +import { QUERY_APP } from '../components/TodoApp'; +import { RelayEnvironmentProvider, useRelayEnvironment } from 'relay-hooks'; let ssrPrefethed = false; -const Routing = ({ssr, variables, prefetch}) => { - const environment = useRelayEnvironment(); +const Routing = ({ ssr, variables, prefetch }) => { + const environment = useRelayEnvironment(); - if (ssr && !ssrPrefethed) { - ssrPrefethed = true; - prefetch.next(environment, QUERY_APP, variables); - } + if (ssr && !ssrPrefethed) { + ssrPrefethed = true; + prefetch.next(environment, QUERY_APP, variables); + } - useEffect(() => { - const handleRouteChange = url => { - const isMe = url === '/'; - prefetch.next(environment, QUERY_APP, {userId: isMe ? 'me' : 'you'}); - }; + useEffect(() => { + const handleRouteChange = (url) => { + const isMe = url === '/'; + console.log('handle', isMe, url); + prefetch.next(environment, QUERY_APP, { userId: isMe ? 'me' : 'you' }); + }; - Router.events.on('routeChangeStart', handleRouteChange); - return () => { - Router.events.off('routeChangeStart', handleRouteChange); - }; - }, [environment]); - return null; + Router.events.on('routeChangeStart', handleRouteChange); + return () => { + Router.events.off('routeChangeStart', handleRouteChange); + }; + }, [environment]); + return null; }; class CustomApp extends App { - static async getInitialProps({Component, ctx}) { - const isServer = !!ctx.req; - let componentProps = {}; + static async getInitialProps({ Component, ctx }) { + const isServer = !!ctx.req; + let componentProps = {}; - if (Component.getInitialProps) { - componentProps = await Component.getInitialProps(ctx); - } + if (Component.getInitialProps) { + componentProps = await Component.getInitialProps(ctx); + } - if (!isServer) { - return { - pageProps: { - prefetch: null, - ssr: false, - environment: null, - }, - }; - } + if (!isServer) { + return { + pageProps: { + prefetch: null, + ssr: false, + environment: null, + }, + }; + } - const isMe = ctx.pathname === '/'; - const {environment, prefetch} = initEnvironment(); + const isMe = ctx.pathname === '/'; + const { environment, prefetch } = initEnvironment(); - const variables = {userId: isMe ? 'me' : 'you'}; - await prefetch.next(environment, QUERY_APP, variables); - const queryRecords = environment - .getStore() - .getSource() - .toJSON(); - const pageProps = { - ...componentProps, - queryRecords, - environment, - variables, - prefetch, - ssr: true, - }; + const variables = { userId: isMe ? 'me' : 'you' }; + await prefetch.next(environment, QUERY_APP, variables); + const queryRecords = environment + .getStore() + .getSource() + .toJSON(); + const pageProps = { + ...componentProps, + queryRecords, + environment, + variables, + prefetch, + ssr: true, + }; - return {pageProps}; - } + return { pageProps }; + } - render() { - const {Component, pageProps} = this.props; - const {environment, prefetch} = - typeof window === 'undefined' - ? pageProps - : initEnvironment({ - records: pageProps.queryRecords, - }); - return ( - - - Relay Hooks NextJS SSR - - - - - - - ); - } + render() { + const { Component, pageProps } = this.props; + const { environment, prefetch } = + typeof window === 'undefined' + ? pageProps + : initEnvironment({ + records: pageProps.queryRecords, + }); + return ( + + + Relay Hooks NextJS SSR + + + + + + + ); + } } export default CustomApp; diff --git a/examples/relay-hook-example/pagination-nextjs-ssr/components/RootPage.tsx b/examples/relay-hook-example/pagination-nextjs-ssr/components/RootPage.tsx index 45d545e1..160270cd 100644 --- a/examples/relay-hook-example/pagination-nextjs-ssr/components/RootPage.tsx +++ b/examples/relay-hook-example/pagination-nextjs-ssr/components/RootPage.tsx @@ -1,7 +1,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import { useRouter } from 'next/router'; import React, { useMemo } from 'react'; -import { useQuery, STORE_OR_NETWORK } from 'relay-hooks'; +import { useQuery, STORE_OR_NETWORK, STORE_THEN_NETWORK } from 'relay-hooks'; import styled from 'styled-components'; import { TodoAppQuery } from '../__generated__/relay/TodoAppQuery.graphql'; import { QUERY_APP, TodoApp } from '../components/TodoApp'; @@ -53,6 +53,8 @@ const RootPage = ({ query, first }: any): JSX.Element => { fetchPolicy: STORE_OR_NETWORK, }); + console.log("render", isLoading) + return (
diff --git a/examples/relay-hook-example/pagination-nextjs-ssr/components/TodoList.tsx b/examples/relay-hook-example/pagination-nextjs-ssr/components/TodoList.tsx index 6625a9cf..7bdaafcd 100644 --- a/examples/relay-hook-example/pagination-nextjs-ssr/components/TodoList.tsx +++ b/examples/relay-hook-example/pagination-nextjs-ssr/components/TodoList.tsx @@ -157,12 +157,13 @@ export const TodoList = (props: Props): JSX.Element => { }, [todos]); const isLoading = - props.isLoading || refetchLoading || (paginated && (isLoadingPrevious || isLoadingNext)); + props.isLoading || refetchLoading || ((paginated || scroll) && (isLoadingPrevious || isLoadingNext)); const loadMore = useCallback(() => { // Don't fetch again if we're already loading the next page if (isLoading) { return; } + console.log("load") loadNext(1); }, [isLoading, loadNext]); @@ -212,7 +213,7 @@ export const TodoList = (props: Props): JSX.Element => { }, [environment, user, onCompleted], ); - + console.log("list render", scroll, hasNext, isLoading) return ( diff --git a/examples/relay-hook-example/pagination-nextjs-ssr/package.json b/examples/relay-hook-example/pagination-nextjs-ssr/package.json index 92c53d4f..4d71f6ee 100644 --- a/examples/relay-hook-example/pagination-nextjs-ssr/package.json +++ b/examples/relay-hook-example/pagination-nextjs-ssr/package.json @@ -33,7 +33,7 @@ "prop-types": "^15.7.2", "react": "^17.0.1", "react-dom": "^17.0.1", - "relay-hooks": "7.0.0", + "relay-hooks": "8.0.0-rc.4", "relay-runtime": "13.0.1", "whatwg-fetch": "3.0.0", "react-infinite-scroller": "1.2.4" @@ -48,6 +48,7 @@ "@babel/runtime": "^7.3.4", "@types/node": "^12.7.12", "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@types/react-relay": "^7.0.2", "@types/relay-runtime": "^13.0.0", "@zeit/next-typescript": "1.1.1", diff --git a/examples/relay-hook-example/pagination-nextjs-ssr/relay/createRelayEnvironment.ts b/examples/relay-hook-example/pagination-nextjs-ssr/relay/createRelayEnvironment.ts index 881bb4a5..df493678 100644 --- a/examples/relay-hook-example/pagination-nextjs-ssr/relay/createRelayEnvironment.ts +++ b/examples/relay-hook-example/pagination-nextjs-ssr/relay/createRelayEnvironment.ts @@ -1,5 +1,5 @@ import fetch from 'isomorphic-unfetch'; -import { Store, Environment, RecordSource, DefaultHandlerProvider, Network } from 'relay-runtime'; +import { Store, Environment, RecordSource, DefaultHandlerProvider, Network, Observable } from 'relay-runtime'; import { HandlerProvider } from 'relay-runtime/lib/handlers/RelayDefaultHandlerProvider'; import { update } from './connection'; @@ -10,10 +10,10 @@ function sleep(ms): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -function fetchQuery(operation, variables, _cacheConfig, _uploadables): Promise { +function fetchQuery(operation, variables, _cacheConfig, _uploadables): any { const endpoint = 'http://localhost:3000/graphql'; - return sleep(500).then(() => - fetch(endpoint, { + return Observable.create((sink) => { + sleep(2000).then(() => fetch(endpoint, { method: 'POST', headers: { Accept: 'application/json', @@ -23,8 +23,16 @@ function fetchQuery(operation, variables, _cacheConfig, _uploadables): Promise response.json()), - ); + }).then(response => response.json()) + .then(data => { + if (data.errors) { + sink.error(data.errors); + return + } + sink.next(data); + sink.complete(); + })); + }); } type InitProps = { diff --git a/examples/suspense/nextjs-ssr/package.json b/examples/suspense/nextjs-ssr/package.json index 08acccc9..cf8f3217 100644 --- a/examples/suspense/nextjs-ssr/package.json +++ b/examples/suspense/nextjs-ssr/package.json @@ -28,7 +28,7 @@ "prop-types": "^15.7.2", "react": "18.2.0", "react-dom": "18.2.0", - "relay-hooks": "7.2.1", + "relay-hooks": "../../../relay-hooks-7.3.0-a5.tgz", "relay-runtime": "14.1.0", "styled-components": "4.4.0", "uuid": "^8.3.2", diff --git a/examples/suspense/nextjs-ssr/tsconfig.json b/examples/suspense/nextjs-ssr/tsconfig.json index 2dba4410..7b31e306 100644 --- a/examples/suspense/nextjs-ssr/tsconfig.json +++ b/examples/suspense/nextjs-ssr/tsconfig.json @@ -18,7 +18,8 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "module": "esnext", - "resolveJsonModule": true + "resolveJsonModule": true, + "incremental": true }, "exclude": [ "node_modules", diff --git a/package-lock.json b/package-lock.json index d98c4ff0..44f620c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "relay-hooks", - "version": "7.2.1", + "version": "8.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "relay-hooks", - "version": "7.2.1", + "version": "8.0.0", "license": "MIT", "dependencies": { "@restart/hooks": "^0.4.9", diff --git a/package.json b/package.json index 6b0fe233..fb30f141 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "relay-hooks", - "version": "7.2.1", + "version": "8.0.0", "keywords": [ "graphql", "relay", diff --git a/src/FetchResolver.ts b/src/FetchResolver.ts index c70b9590..a6d66ffb 100644 --- a/src/FetchResolver.ts +++ b/src/FetchResolver.ts @@ -18,8 +18,8 @@ export type Fetcher = { environment: IEnvironment, operation: OperationDescriptor, fetchPolicy: FetchPolicy | null | undefined, - onComplete: (_e: Error | null) => void, - onNext: (operation: OperationDescriptor, snapshot: Snapshot, fromStore?: boolean, onlyStore?: boolean) => void, + onComplete: (_e: Error | null, doUpdate: boolean) => void, + onNext: (operation: OperationDescriptor, snapshot: Snapshot, doUpdate: boolean) => void, onResponse?: (response: GraphQLResponse | null) => void, renderPolicy?: RenderPolicy, ) => Disposable; @@ -32,12 +32,10 @@ export type Fetcher = { }; export function fetchResolver({ - setLoading, doRetain = true, disposeTemporary, }: { doRetain?: boolean; - setLoading?: (loading: boolean) => void; disposeTemporary?: () => void; }): Fetcher { let _refetchSubscription: Subscription | null = null; @@ -49,9 +47,9 @@ export function fetchResolver({ let error: Error | null = null; let env; - const updateLoading = (loading: boolean): void => { + const update = (loading: boolean, e: Error = null): void => { isLoading = loading; - setLoading && setLoading(isLoading); + error = e; }; const lookupInStore = ( environment: IEnvironment, @@ -97,17 +95,19 @@ export function fetchResolver({ const disposeRequest = (): void => { _refetchSubscription && _refetchSubscription.unsubscribe(); error = null; + isLoading = false; }; const fetch = ( environment: IEnvironment, operation: OperationDescriptor, fetchPolicy: FetchPolicy = 'network-only', - onComplete = (_e: Error | null): void => undefined, - onNext: (operation: OperationDescriptor, snapshot: Snapshot, fromStore?: boolean, onlyStore?: boolean) => void, + onComplete = (_e: Error | null, _u: boolean): void => undefined, + onNext: (operation: OperationDescriptor, snapshot: Snapshot, doUpdate: boolean) => void, onResponse?: (response: GraphQLResponse | null) => void, renderPolicy?: RenderPolicy, ): Disposable => { + let fetchHasReturned = false; if (env != environment || query.request.identifier !== operation.request.identifier) { dispose(); if (doRetain) { @@ -122,19 +122,19 @@ export function fetchResolver({ const isNetwork = isNetworkPolicy(fetchPolicy, full); if (snapshot != null) { const onlyStore = !isNetwork; - onNext(operation, snapshot, true, onlyStore); + onNext(operation, snapshot, fetchHasReturned && !onlyStore); if (onlyStore) { - onComplete(null); + onComplete(null, fetchHasReturned); } } // Cancel any previously running refetch. _refetchSubscription && _refetchSubscription.unsubscribe(); + let refetchSubscription: Subscription; if (isNetwork) { let resolveNetworkPromise = (): void => {}; // Declare refetchSubscription before assigning it in .start(), since // synchronous completion may call callbacks .subscribe() returns. - let refetchSubscription: Subscription; const cleanup = (): void => { if (_refetchSubscription === refetchSubscription) { _refetchSubscription = null; @@ -143,35 +143,35 @@ export function fetchResolver({ promise = null; }; + const complete = (error: Error = null) => { + resolveNetworkPromise(); + update(false, error); + cleanup(); + onComplete(error, fetchHasReturned); + }; + fetchQuery(environment, operation).subscribe({ unsubscribe: (): void => { cleanup(); }, - complete: (): void => { - resolveNetworkPromise(); - updateLoading(false); - cleanup(); - onComplete(null); - }, - error: (e: Error): void => { - error = e; - resolveNetworkPromise(); - updateLoading(false); - cleanup(); - onComplete(e); - }, + complete, + error: (e: Error): void => complete(e), next: (response: GraphQLResponse) => { const store = environment.lookup(operation.fragment); promise = null; - operation.request.cacheConfig?.poll && updateLoading(false); + const responses = Array.isArray(response) ? response : [response]; + const cacheConfig = operation.request.cacheConfig; + const isQueryPolling = !!cacheConfig && !!cacheConfig.poll; + const isIncremental = responses.some((x) => x != null && x.hasNext === true); + isQueryPolling && update(false); resolveNetworkPromise(); onResponse && onResponse(response); - onNext(operation, store); + onNext(operation, store, fetchHasReturned && (isIncremental || isQueryPolling)); }, start: (subscription) => { refetchSubscription = subscription; _refetchSubscription = refetchSubscription; - updateLoading(true); + update(true); }, }); if (!snapshot) { @@ -179,14 +179,12 @@ export function fetchResolver({ resolveNetworkPromise = resolve; }); } - return { - dispose: (): void => { - refetchSubscription && refetchSubscription.unsubscribe(); - }, - }; } + fetchHasReturned = true; return { - dispose: (): void => {}, + dispose: (): void => { + refetchSubscription && refetchSubscription.unsubscribe(); + }, }; }; diff --git a/src/FragmentResolver.ts b/src/FragmentResolver.ts index 252202a8..7718b884 100644 --- a/src/FragmentResolver.ts +++ b/src/FragmentResolver.ts @@ -30,6 +30,9 @@ const { getPromiseForActiveRequest } = __internal; type SingularOrPluralSnapshot = Snapshot | Array; +// eslint-disable-next-line @typescript-eslint/no-empty-function +function emptyVoid() {} + function lookupFragment(environment, selector): SingularOrPluralSnapshot { return selector.kind === 'PluralReaderSelector' ? selector.selectors.map((s) => environment.lookup(s)) @@ -117,32 +120,33 @@ export class FragmentResolver { pagination = false; result: any; _subscribeResolve; + forceUpdate; constructor(name: FragmentNames) { this.name = name; this.pagination = name === PAGINATION_NAME; this.refetchable = name === REFETCHABLE_NAME || this.pagination; - const setLoading = (_loading): void => this.refreshHooks(); if (this.refetchable) { this.fetcherRefecth = fetchResolver({ - setLoading, doRetain: true, }); } if (this.pagination) { - this.fetcherNext = fetchResolver({ setLoading }); - this.fetcherPrevious = fetchResolver({ setLoading }); + this.fetcherNext = fetchResolver({}); + this.fetcherPrevious = fetchResolver({}); } - } - - setForceUpdate(forceUpdate = (): void => undefined): void { + this.setForceUpdate(); this.refreshHooks = (): void => { this.resolveResult(); - forceUpdate(); + this.forceUpdate(); }; } + setForceUpdate(forceUpdate = emptyVoid): void { + this.forceUpdate = forceUpdate; + } + subscribeResolve(subscribeResolve: (data: any) => void): void { if (this._subscribeResolve && this._subscribeResolve != subscribeResolve) { subscribeResolve(this.getData()); @@ -259,7 +263,7 @@ export class FragmentResolver { // $FlowExpectedError[prop-missing] Expando to annotate Promises. (promise as any).displayName = 'Relay(' + parentQueryName + ')'; this.unsubscribe(); - this.refreshHooks = (): void => undefined; + this.refreshHooks = emptyVoid; throw promise; } warning( @@ -353,7 +357,7 @@ export class FragmentResolver { environment.subscribe(snapshot, (latestSnapshot) => { this.resolverData.snapshot[idx] = latestSnapshot; this.resolverData.data[idx] = latestSnapshot.data; - this.resolverData.isMissingData = false; + this.resolverData.isMissingData = isMissingData(this.resolverData.snapshot); this.refreshHooks(); }), ); @@ -362,7 +366,6 @@ export class FragmentResolver { dataSubscriptions.push( environment.subscribe(renderedSnapshot, (latestSnapshot) => { this.resolverData = getFragmentResult(latestSnapshot); - this.resolverData.isMissingData = false; this.refreshHooks(); }), ); @@ -377,7 +380,8 @@ export class FragmentResolver { }; } - refetch = (variables: Variables, options?: Options): Disposable => { + refetch = (variables: Variables, options: Options = {}): Disposable => { + const name = this.name; if (this.unmounted === true) { warning( false, @@ -387,9 +391,9 @@ export class FragmentResolver { 'Please make sure you clear all timers, intervals, ' + 'async calls, etc that may trigger a fetch.', this._fragment.name, - this.name, + name, ); - return { dispose: (): void => {} }; + return { dispose: emptyVoid }; } if (this._selector == null) { warning( @@ -400,14 +404,14 @@ export class FragmentResolver { 'passing a valid fragment ref to `%s` before calling ' + '`refetch`, or make sure you pass all required variables to `refetch`.', this._fragment.name, - this.name, - this.name, + name, + name, ); } const { fragmentRefPathInResponse, identifierField, refetchableRequest } = getRefetchMetadata( this._fragment, - this.name, + name, ); const fragmentData = this.getData().data; const identifierValue = @@ -459,7 +463,7 @@ export class FragmentResolver { refetchVariables.id = identifierValue; } - const onNext = (operation: OperationDescriptor, snapshot: Snapshot): void => { + const onNext = (operation: OperationDescriptor, snapshot: Snapshot, doUpdate: boolean): void => { const fragmentRef = getValueAtPath(snapshot.data, fragmentRefPathInResponse); const isEquals = this.isEqualsFragmentRef(this._fragmentRefRefetch || this._fragmentRef, fragmentRef); const missData = isMissingData(snapshot); //fromStore && isMissingData(snapshot); @@ -473,23 +477,30 @@ export class FragmentResolver { }*/ this.resolverData.isMissingData = missData; this.resolverData.owner = operation.request; - this.refreshHooks(); + doUpdate && this.refreshHooks(); } }; if (this.pagination) { this.fetcherNext.dispose(); this.fetcherPrevious.dispose(); } + const complete = (error, doUpdate) => { + doUpdate && this.refreshHooks(); + options.onComplete && options.onComplete(error); + }; + const operation = createOperation(refetchableRequest, refetchVariables, forceCache); - return this.fetcherRefecth.fetch( + const disposable = this.fetcherRefecth.fetch( this._environment, operation, - options?.fetchPolicy, - options?.onComplete, + options.fetchPolicy, + complete, onNext, - options?.onResponse, - options?.UNSTABLE_renderPolicy, + options.onResponse, + options.UNSTABLE_renderPolicy, ); + this.refreshHooks(); + return disposable; }; loadPrevious = (count: number, options?: OptionsLoadMore): Disposable => { @@ -500,10 +511,11 @@ export class FragmentResolver { return this.loadMore('forward', count, options); }; - loadMore = (direction: 'backward' | 'forward', count: number, options?: OptionsLoadMore): Disposable => { - const onComplete = options?.onComplete ?? ((): void => undefined); + loadMore = (direction: 'backward' | 'forward', count: number, options: OptionsLoadMore = {}): Disposable => { + const onComplete = options.onComplete ?? emptyVoid; const fragmentData = this.getData().data; + const emptyDispose = { dispose: emptyVoid }; const fetcher = direction === 'backward' ? this.fetcherPrevious : this.fetcherNext; if (this.unmounted === true) { @@ -519,7 +531,7 @@ export class FragmentResolver { this._fragment.name, this.name, ); - return { dispose: (): void => {} }; + return emptyDispose; } if (this._selector == null) { warning( @@ -533,14 +545,14 @@ export class FragmentResolver { this.name, ); onComplete(null); - return { dispose: (): void => {} }; + return emptyDispose; } const isRequestActive = (this._environment as any).isRequestActive( (this._selector as SingularReaderSelector).owner.identifier, ); if (isRequestActive || fetcher.getData().isLoading === true || fragmentData == null) { onComplete(null); - return { dispose: (): void => {} }; + return emptyDispose; } invariant( this._selector != null && this._selector.kind !== 'PluralReaderSelector', @@ -560,7 +572,7 @@ export class FragmentResolver { const parentVariables = (this._selector as SingularReaderSelector).owner.variables; const fragmentVariables = (this._selector as SingularReaderSelector).variables; - const extraVariables = options?.UNSTABLE_extraVariables; + const extraVariables = options.UNSTABLE_extraVariables; const baseVariables = { ...parentVariables, ...fragmentVariables, @@ -592,16 +604,21 @@ export class FragmentResolver { paginationVariables.id = identifierValue; } - const onNext = (): void => {}; + const complete = (error, doUpdate) => { + if (doUpdate) this.refreshHooks(); + onComplete(error); + }; const operation = createOperation(paginationRequest, paginationVariables, forceCache); - return fetcher.fetch( + const disposable = fetcher.fetch( this._environment, operation, undefined, //options?.fetchPolicy, - onComplete, - onNext, - options?.onResponse, + complete, + emptyVoid, + options.onResponse, ); + this.refreshHooks(); + return disposable; }; } diff --git a/src/QueryFetcher.ts b/src/QueryFetcher.ts index a579bbbf..d2f622ca 100644 --- a/src/QueryFetcher.ts +++ b/src/QueryFetcher.ts @@ -115,22 +115,22 @@ export class QueryFetcher } const { onComplete, onResponse } = options; - let fetchHasReturned = false; - const onNext = (_o: OperationDescriptor, snapshot: Snapshot): void => { + const resolveUpdate = (doUpdate) => { + this.resolveResult(); + if (doUpdate) { + this.forceUpdate(); + } + }; + const onNext = (operation: OperationDescriptor, snapshot: Snapshot, doUpdate: boolean): void => { if (!this.snapshot) { this.snapshot = snapshot; this.subscribe(snapshot); - this.resolveResult(); - if (fetchHasReturned) { - this.forceUpdate(); - } + resolveUpdate(doUpdate); } }; - const complete = (error: Error | null): void => { - this.resolveResult(); - if (fetchHasReturned) { - this.forceUpdate(); - } + const complete = (error: Error | null, doUpdate: boolean): void => { + // doUpdate is False only if fetch is Sync + resolveUpdate(doUpdate); onComplete && onComplete(error); }; this.fetcher.fetch( @@ -142,7 +142,6 @@ export class QueryFetcher onResponse, options.UNSTABLE_renderPolicy, ); - fetchHasReturned = true; } getQuery(gqlQuery, variables, networkCacheConfig): OperationDescriptor | null { @@ -220,7 +219,6 @@ export class QueryFetcher // Read from this._fetchOptions in case onDataChange() was lazily added. this.snapshot = snapshot; //this.error = null; - this.resolveResult(); this.forceUpdate(); }); diff --git a/src/useForceUpdate.ts b/src/useForceUpdate.ts index c98cabb8..2333e712 100644 --- a/src/useForceUpdate.ts +++ b/src/useForceUpdate.ts @@ -1,6 +1,18 @@ -import { Reducer, useReducer } from 'react'; +import { Reducer, useCallback, useEffect, useReducer, useRef } from 'react'; export function useForceUpdate(): () => void { const [, forceUpdate] = useReducer>((x) => x + 1, 0); - return forceUpdate as () => void; + const isMounted = useRef(false); + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + const update = useCallback(() => { + if (isMounted.current) { + forceUpdate(); + } + }, [isMounted, forceUpdate]); + return update; } diff --git a/src/useOssFragment.tsx b/src/useOssFragment.tsx index 6d77cbb9..e241fcf8 100644 --- a/src/useOssFragment.tsx +++ b/src/useOssFragment.tsx @@ -55,7 +55,6 @@ export function useOssFragment( resolver.resolve(environment, idfragment, fragment, fragmentRef); if (subscribeResolve) { - resolver.setForceUpdate(); return; }