diff --git a/.prettierrc b/.prettierrc index 5921dd0..f74ad94 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "trailingComma": "all", "arrowParens": "always", "printWidth": 140, - "parser": "typescript" + "parser": "typescript", + "endOfLine": "auto" } \ No newline at end of file diff --git a/README.md b/README.md index 76ff010..a142028 100644 --- a/README.md +++ b/README.md @@ -77,39 +77,20 @@ You then need to link the native parts of the library for the platforms you are Memorang -## React Web Example +## react-relay-offline examples -The [react-relay-offline-examples](https://github.com/morrys/react-relay-offline-example) repository contains an integration of react-relay-offline. To try it out: +The [offline-examples](https://github.com/morrys/offline-examples) repository contains example projects on how to use react-relay-offline: -``` -git clone https://github.com/morrys/react-relay-offline-example.git -cd react-relay-offline-example/todo -yarn -yarn build -yarn start -``` - -Then, just point your browser at `http://localhost:3000`. - -or - -``` -git clone https://github.com/morrys/react-relay-offline-example.git -cd react-relay-offline-example/todo-updater -yarn -yarn build -yarn start -``` + * `nextjs-ssr-preload`: using the render-as-you-fetch pattern with loadQuery in SSR contexts + * `nextjs`: using the QueryRenderer in SSR contexts + * `react-native/todo-updater`: using QueryRender in an RN application + * `todo-updater`: using the QueryRender + * `suspense/cra`: using useLazyLoadQuery in a CRA + * `suspense/nextjs-ssr-preload`: using the render-as-you-fetch pattern with loadLazyQuery in react concurrent + SSR contexts + * `suspense/nextjs-ssr`: using useLazyLoadQuery in SSR contexts -Then, just point your browser at `http://localhost:3000`. +To try it out! -## React NextJS Offline SSR Example - -The [React NextJS Offline SSR Example](https://github.com/morrys/offline-examples/tree/master/relay/nextjs) - -## React Native Example - -The [react native offline example](https://github.com/morrys/offline-examples#react-native) ## Environment @@ -259,10 +240,35 @@ import { QueryRenderer } from 'react-relay-offline'; render={({ props, error, retry, cached }) => { ``` +## useQuery + +```ts +import { useQuery } from "react-relay-offline"; +const hooksProps = useQuery(query, variables, { + networkCacheConfig: cacheConfig, + fetchPolicy, + ttl +}); +``` + +## useLazyLoadQuery + +```ts +import { useQuery } from "react-relay-offline"; +const hooksProps = useLazyLoadQuery(query, variables, { + networkCacheConfig: cacheConfig, + fetchPolicy, + ttl +}); +``` + ## useRestore & loading -the **useRestore** hook allows you to manage the restore of data persisted in the storage. -**To be used if relay components are used outside of the QueryRenderer** or **for web applications without SSR & react-native** ( +the **useRestore** hook allows you to manage the hydratation of persistent data in memory and to initialize the environment. + +**It must always be used before using environement in web applications without SSR & react legacy & react-native.** + +**Otherwise, for SSR and react concurrent applications the restore is natively managed by QueryRenderer & useQueryLazyLoad & useQuery.** ``` const isRehydrated = useRestore(environment); @@ -271,9 +277,6 @@ const isRehydrated = useRestore(environment); } ``` -**For SSR web applications there is a native management in the QueryRenderer to correctly manage the DOM returned by the server and restore the environment** - - ## fetchQuery ```ts @@ -294,24 +297,76 @@ import { useNetInfo } from "react-relay-offline"; import { NetInfo } from "react-relay-offline"; ``` -## Hooks & useQuery +## Supports Hooks from relay-hooks Now you can use hooks (useFragment, usePagination, useRefetch) from [relay-hooks](https://github.com/relay-tools/relay-hooks) -while it is necessary to use `useQuery` of react-relay-offline to manage the offline. +## render-as-you-fetch & usePreloadedQuery + +### loadQuery + +* input parameters + +same as useQuery + environment + +* output parameters + * + `next: ( + environment: Environment, + gqlQuery: GraphQLTaggedNode, + variables?: TOperationType['variables'], + options?: QueryOptions, + ) => Promise`: fetches data. A promise returns to allow the await in case of SSR + * `dispose: () => void`: cancel the subscription and dispose of the fetch + * `subscribe: (callback: (value: any) => any) => () => void`: used by the usePreloadedQuery + * `getValue (environment?: Environment,) => OfflineRenderProps | Promise`: used by the usePreloadedQuery ```ts -import { useQuery } from "react-relay-offline"; -const hooksProps = useQuery(query, variables, { - networkCacheConfig: cacheConfig, - fetchPolicy, - ttl -}); +import {graphql, loadQuery} from 'react-relay-offline'; +import {environment} from ''./environment'; + +const query = graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } +`; + +const prefetch = loadQuery(); +prefetch.next( + environment, + query, + {id: '4'}, + {fetchPolicy: 'store-or-network'}, +); +// pass prefetch to usePreloadedQuery() +``` + +### loadLazyQuery + +**is the same as loadQuery but must be used with suspense** + +### render-as-you-fetch in SSR + +In SSR contexts, **not using the useRestore hook** it is necessary to manually invoke the hydrate but without using the await. + +This will allow the usePreloadedQuery hook to correctly retrieve the data from the store and once the hydration is done it will be react-relay-offline + +to notify any updated data in the store. + +```ts + if (!environment.isRehydrated() && ssr) { + environment.hydrate().then(() => {}).catch((error) => {}); + } + prefetch.next(environment, QUERY_APP, variables, { + fetchPolicy: NETWORK_ONLY, + }); ``` ## Requirement -- Version >=6.0.0 of the react-relay library +- Version >=8.0.0 of the react-relay library - When a new node is created by mutation the id must be generated in the browser to use it in the optimistic response ## License diff --git a/__tests__/ReactRelayQueryRenderer-test.tsx b/__tests__/ReactRelayQueryRenderer-test.tsx index 93ace99..91a1328 100644 --- a/__tests__/ReactRelayQueryRenderer-test.tsx +++ b/__tests__/ReactRelayQueryRenderer-test.tsx @@ -184,6 +184,8 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -242,7 +244,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added + rehydrated: true, + online: true, // added }).toBeRendered(); expect(environment.execute.mock.calls.length).toBe(1); render.mockClear(); @@ -256,7 +259,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added + rehydrated: true, + online: true, // added }).toBeRendered(); }); }); @@ -326,6 +330,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); expect(environment.execute.mock.calls.length).toBe(1); @@ -351,6 +356,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -416,6 +422,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); expect(fetch.mock.calls.length).toBe(1); @@ -441,6 +448,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -494,6 +502,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); render.mockClear(); @@ -554,6 +563,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }); }); @@ -587,7 +597,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); @@ -630,6 +641,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -670,6 +682,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -699,6 +712,7 @@ describe('ReactRelayQueryRenderer', () => { error: error, props: null, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -770,6 +784,7 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, rehydrated: true, + online: true, retry: expect.any(Function), }; expect(readyState).toBeRendered(); @@ -800,7 +815,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); @@ -827,7 +843,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); @@ -855,7 +872,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); @@ -891,7 +909,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); }); @@ -920,6 +939,7 @@ describe('ReactRelayQueryRenderer', () => { error, props: null, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -953,6 +973,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -1011,6 +1032,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }, ], @@ -1032,6 +1054,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }, ], @@ -1082,6 +1105,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online: true, retry: expect.any(Function), }).toBeRendered(); }); @@ -1157,7 +1181,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); }); @@ -1224,7 +1249,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); @@ -1315,7 +1341,8 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, retry: expect.any(Function), - rehydrated: true, // added, + rehydrated: true, + online: true, // added, }).toBeRendered(); }); diff --git a/__tests__/RelayOfflineHydrate-test.tsx b/__tests__/RelayOfflineHydrate-test.tsx index 2f0d35b..73f3d3a 100644 --- a/__tests__/RelayOfflineHydrate-test.tsx +++ b/__tests__/RelayOfflineHydrate-test.tsx @@ -90,7 +90,7 @@ describe('ReactRelayQueryRenderer', () => { 'node(id:"4")': { __ref: '4' }, }, }; - const propsInitialState = (owner, rehydrated) => { + const propsInitialState = (owner, rehydrated, online = rehydrated) => { return { error: null, props: { @@ -107,10 +107,11 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated, + online retry: expect.any(Function), }; }; - const propsRestoredState = (owner) => { + const propsRestoredState = (owner, online = true) => { return { error: null, props: { @@ -127,6 +128,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated: true, + online, retry: expect.any(Function), }; }; @@ -135,6 +137,16 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, rehydrated: true, + online: true, + retry: expect.any(Function), + // @ts-ignore + }; + + const loadingStateRehydratedOffline = { + error: null, + props: null, + rehydrated: true, + online: false, retry: expect.any(Function), // @ts-ignore }; @@ -143,6 +155,7 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, rehydrated: false, + online: false, retry: expect.any(Function), // @ts-ignore }; @@ -404,7 +417,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydrated).toBeRendered(); + expect(loadingStateRehydratedOffline).toBeRendered(); }); it('without useRestore', () => { @@ -421,7 +434,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydrated).toBeRendered(); + expect(loadingStateRehydratedOffline).toBeRendered(); }); }); describe('initial state', () => { @@ -447,7 +460,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsInitialState(owner, true)).toBeRendered(); + expect(propsInitialState(owner, true, false)).toBeRendered(); }); it('without useRestore', () => { @@ -492,7 +505,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner)).toBeRendered(); + expect(propsRestoredState(owner, false)).toBeRendered(); }); it('without useRestore', () => { @@ -509,7 +522,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner)).toBeRendered(); + expect(propsRestoredState(owner, false)).toBeRendered(); }); }); @@ -536,7 +549,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner)).toBeRendered(); + expect(propsRestoredState(owner, false)).toBeRendered(); }); /* @@ -557,7 +570,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydrated).toBeRendered(); + expect(loadingStateRehydratedOffline).toBeRendered(); }); }); }); diff --git a/__tests__/RelayOfflineTTL-test.tsx b/__tests__/RelayOfflineTTL-test.tsx index 78fefaa..1690e95 100644 --- a/__tests__/RelayOfflineTTL-test.tsx +++ b/__tests__/RelayOfflineTTL-test.tsx @@ -57,7 +57,7 @@ describe('ReactRelayQueryRenderer', () => { let initialData; let owner; const variables = { id: '4' }; - const propsInitialState = (owner, rehydrated) => { + const propsInitialState = (owner, rehydrated, online = rehydrated) => { return { error: null, props: { @@ -74,6 +74,7 @@ describe('ReactRelayQueryRenderer', () => { }, }, rehydrated, + online, retry: expect.any(Function), }; }; @@ -82,6 +83,7 @@ describe('ReactRelayQueryRenderer', () => { error: null, props: null, rehydrated: true, + online: true, retry: expect.any(Function), // @ts-ignore }; diff --git a/docs/ReactRelayOffline-Introduction.md b/docs/ReactRelayOffline-Introduction.md index a22fd60..a142028 100644 --- a/docs/ReactRelayOffline-Introduction.md +++ b/docs/ReactRelayOffline-Introduction.md @@ -77,39 +77,20 @@ You then need to link the native parts of the library for the platforms you are Memorang -## React Web Example +## react-relay-offline examples -The [react-relay-offline-examples](https://github.com/morrys/react-relay-offline-example) repository contains an integration of react-relay-offline. To try it out: +The [offline-examples](https://github.com/morrys/offline-examples) repository contains example projects on how to use react-relay-offline: -``` -git clone https://github.com/morrys/react-relay-offline-example.git -cd react-relay-offline-example/todo -yarn -yarn build -yarn start -``` - -Then, just point your browser at `http://localhost:3000`. - -or - -``` -git clone https://github.com/morrys/react-relay-offline-example.git -cd react-relay-offline-example/todo-updater -yarn -yarn build -yarn start -``` + * `nextjs-ssr-preload`: using the render-as-you-fetch pattern with loadQuery in SSR contexts + * `nextjs`: using the QueryRenderer in SSR contexts + * `react-native/todo-updater`: using QueryRender in an RN application + * `todo-updater`: using the QueryRender + * `suspense/cra`: using useLazyLoadQuery in a CRA + * `suspense/nextjs-ssr-preload`: using the render-as-you-fetch pattern with loadLazyQuery in react concurrent + SSR contexts + * `suspense/nextjs-ssr`: using useLazyLoadQuery in SSR contexts -Then, just point your browser at `http://localhost:3000`. +To try it out! -## React NextJS Offline SSR Example - -The [React NextJS Offline SSR Example](https://github.com/morrys/offline-examples/tree/master/relay/nextjs) - -## React Native Example - -The [react native offline example](https://github.com/morrys/offline-examples#react-native) ## Environment @@ -259,10 +240,35 @@ import { QueryRenderer } from 'react-relay-offline'; render={({ props, error, retry, cached }) => { ``` +## useQuery + +```ts +import { useQuery } from "react-relay-offline"; +const hooksProps = useQuery(query, variables, { + networkCacheConfig: cacheConfig, + fetchPolicy, + ttl +}); +``` + +## useLazyLoadQuery + +```ts +import { useQuery } from "react-relay-offline"; +const hooksProps = useLazyLoadQuery(query, variables, { + networkCacheConfig: cacheConfig, + fetchPolicy, + ttl +}); +``` + ## useRestore & loading -the **useRestore** hook allows you to manage the restore of data persisted in the storage. -**To be used if relay components are used outside of the QueryRenderer** or **for web applications without SSR & react-native** ( +the **useRestore** hook allows you to manage the hydratation of persistent data in memory and to initialize the environment. + +**It must always be used before using environement in web applications without SSR & react legacy & react-native.** + +**Otherwise, for SSR and react concurrent applications the restore is natively managed by QueryRenderer & useQueryLazyLoad & useQuery.** ``` const isRehydrated = useRestore(environment); @@ -291,24 +297,76 @@ import { useNetInfo } from "react-relay-offline"; import { NetInfo } from "react-relay-offline"; ``` -## Hooks & useQuery +## Supports Hooks from relay-hooks Now you can use hooks (useFragment, usePagination, useRefetch) from [relay-hooks](https://github.com/relay-tools/relay-hooks) -while it is necessary to use `useQuery` of react-relay-offline to manage the offline. +## render-as-you-fetch & usePreloadedQuery + +### loadQuery + +* input parameters + +same as useQuery + environment + +* output parameters + * + `next: ( + environment: Environment, + gqlQuery: GraphQLTaggedNode, + variables?: TOperationType['variables'], + options?: QueryOptions, + ) => Promise`: fetches data. A promise returns to allow the await in case of SSR + * `dispose: () => void`: cancel the subscription and dispose of the fetch + * `subscribe: (callback: (value: any) => any) => () => void`: used by the usePreloadedQuery + * `getValue (environment?: Environment,) => OfflineRenderProps | Promise`: used by the usePreloadedQuery ```ts -import { useQuery } from "react-relay-offline"; -const hooksProps = useQuery(query, variables, { - networkCacheConfig: cacheConfig, - fetchPolicy, - ttl -}); +import {graphql, loadQuery} from 'react-relay-offline'; +import {environment} from ''./environment'; + +const query = graphql` + query AppQuery($id: ID!) { + user(id: $id) { + name + } + } +`; + +const prefetch = loadQuery(); +prefetch.next( + environment, + query, + {id: '4'}, + {fetchPolicy: 'store-or-network'}, +); +// pass prefetch to usePreloadedQuery() +``` + +### loadLazyQuery + +**is the same as loadQuery but must be used with suspense** + +### render-as-you-fetch in SSR + +In SSR contexts, **not using the useRestore hook** it is necessary to manually invoke the hydrate but without using the await. + +This will allow the usePreloadedQuery hook to correctly retrieve the data from the store and once the hydration is done it will be react-relay-offline + +to notify any updated data in the store. + +```ts + if (!environment.isRehydrated() && ssr) { + environment.hydrate().then(() => {}).catch((error) => {}); + } + prefetch.next(environment, QUERY_APP, variables, { + fetchPolicy: NETWORK_ONLY, + }); ``` ## Requirement -- Version >=6.0.0 of the react-relay library +- Version >=8.0.0 of the react-relay library - When a new node is created by mutation the id must be generated in the browser to use it in the optimistic response ## License diff --git a/package-lock.json b/package-lock.json index d7596d6..23a14f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6327,9 +6327,9 @@ "dev": true }, "@restart/hooks": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.22.tgz", - "integrity": "sha512-tW0T3hP6emYNOc76/iC96rlu+f7JYLSVk/Wnn+7dj1gJUcw4CkQNLy16vx2mBLtVKsFMZ9miVEZXat8blruDHQ==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.25.tgz", + "integrity": "sha512-m2v3N5pxTsIiSH74/sb1yW8D9RxkJidGW+5Mfwn/lHb2QzhZNlaU1su7abSyT9EGf0xS/0waLjrf7/XxQHUk7w==", "requires": { "lodash": "^4.17.15", "lodash-es": "^4.17.15" @@ -6503,6 +6503,12 @@ "@types/react": "*" } }, + "@types/relay-runtime": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/@types/relay-runtime/-/relay-runtime-9.1.6.tgz", + "integrity": "sha512-rdylQOGAoqjDjtzC2uKdjIqeDRcAvUsjONQB+FZld3cAP9qU4eEwTfMKJ7MdZSw2YbWRcTphczT4OpKKlLxY3Q==", + "dev": true + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -18614,9 +18620,9 @@ } }, "relay-hooks": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/relay-hooks/-/relay-hooks-3.4.0.tgz", - "integrity": "sha512-HU1s4H492V/riq5N9y9ddNZ14xbgPsAWaa4AaoHkAJ24LGmo0BzVAIM0x0T/wo8awGL7x9Kpjc56pNbdR76N+g==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/relay-hooks/-/relay-hooks-3.7.0.tgz", + "integrity": "sha512-tZnS2rjeOvWpMAKIGS0w09InX//yigtyNg2ic43aAAZaLnjnp6iCJyn7HXwPx3SO/5tCymjgF/hMBicipZJnTw==", "requires": { "@restart/hooks": "^0.3.1", "fbjs": "^1.0.0" diff --git a/package.json b/package.json index 8042051..e999bf9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-relay-offline", - "version": "2.2.0", + "version": "2.3.0", "keywords": [ "graphql", "relay", @@ -38,7 +38,7 @@ "fbjs": "^1.0.0", "nullthrows": "^1.1.0", "uuid": "3.3.2", - "relay-hooks": "^3.4.0", + "relay-hooks": "^3.7.0", "tslib": "^1.11.1" }, "peerDependencies": { @@ -53,6 +53,7 @@ "@types/promise-polyfill": "^6.0.3", "@types/react": "16.8.14", "@types/react-dom": "16.8.4", + "@types/relay-runtime": "^9.0.0", "@typescript-eslint/eslint-plugin": "2.24.0", "@typescript-eslint/parser": "2.24.0", "babel-jest": "24.9.0", diff --git a/src/QueryRendererHook.tsx b/src/QueryRendererHook.tsx index 8940c2f..77781c9 100644 --- a/src/QueryRendererHook.tsx +++ b/src/QueryRendererHook.tsx @@ -1,14 +1,17 @@ import * as React from 'react'; import { QueryRendererProps } from './RelayOfflineTypes'; import { useQueryOffline } from './hooks/useQueryOffline'; +import { OperationType } from 'relay-runtime'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const QueryRendererHook = (props: QueryRendererProps) => { - const { render, fetchPolicy, query, variables, cacheConfig, ttl } = props; +export const QueryRendererHook = (props: QueryRendererProps) => { + const { render, fetchPolicy, query, variables, cacheConfig, ttl, fetchKey, fetchObserver } = props; const hooksProps = useQueryOffline(query, variables, { networkCacheConfig: cacheConfig, fetchPolicy, ttl, + fetchKey, + fetchObserver, }); return {render(hooksProps)}; diff --git a/src/QueryRendererOffline.tsx b/src/QueryRendererOffline.tsx index f480589..5f10eeb 100644 --- a/src/QueryRendererOffline.tsx +++ b/src/QueryRendererOffline.tsx @@ -2,9 +2,10 @@ import * as React from 'react'; import { QueryRendererHook } from './QueryRendererHook'; import { RelayEnvironmentProvider } from 'relay-hooks'; import { QueryRendererOfflineProps } from './RelayOfflineTypes'; +import { OperationType } from 'relay-runtime'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const QueryRendererOffline = function (props: QueryRendererOfflineProps) { +export const QueryRendererOffline = function (props: QueryRendererOfflineProps) { const { environment } = props; return ( diff --git a/src/RelayOfflineTypes.ts b/src/RelayOfflineTypes.ts index dab6c55..5ce0ca5 100644 --- a/src/RelayOfflineTypes.ts +++ b/src/RelayOfflineTypes.ts @@ -1,21 +1,43 @@ -import { OperationType, CacheConfig, GraphQLTaggedNode } from 'relay-runtime'; -import { FetchPolicy, RenderProps } from 'relay-hooks'; +import { OperationType, CacheConfig, GraphQLTaggedNode, Observer, Snapshot } from 'relay-runtime'; +import { FetchPolicy, RenderProps, QueryOptions, LoadQuery } from 'relay-hooks'; +import { Environment } from '@wora/relay-offline'; -export interface QueryRendererProps extends QueryProps { - render: (renderProps: RenderProps) => React.ReactNode; +export interface QueryRendererProps extends QueryProps { + render: (renderProps: OfflineRenderProps) => React.ReactNode; } -export interface QueryRendererOfflineProps extends QueryRendererProps { - environment: any; +export interface QueryRendererOfflineProps extends QueryRendererProps { + environment: Environment; } -export interface OfflineRenderProps extends RenderProps { +export interface OfflineRenderProps extends RenderProps { rehydrated: boolean; + online: boolean; } + +export interface OfflineLoadQuery extends LoadQuery { + getValue: ( + environment?: Environment, + ) => OfflineRenderProps | Promise; + next: ( + environment: Environment, + gqlQuery: GraphQLTaggedNode, + variables?: TOperationType['variables'], + options?: QueryOptions, + ) => Promise; +} + export interface QueryProps { cacheConfig?: CacheConfig; fetchPolicy?: FetchPolicy; + fetchKey?: string | number; query: GraphQLTaggedNode; variables: T['variables']; ttl?: number; + skip?: boolean; + fetchObserver?: Observer; } + +export type QueryOptionsOffline = QueryOptions & { + ttl?: number; +}; diff --git a/src/hooks/useLazyLoadQueryOffline.ts b/src/hooks/useLazyLoadQueryOffline.ts new file mode 100644 index 0000000..03aeef5 --- /dev/null +++ b/src/hooks/useLazyLoadQueryOffline.ts @@ -0,0 +1,63 @@ +import { useRef } from 'react'; +import { useRelayEnvironment, useQueryFetcher, STORE_ONLY } from 'relay-hooks'; +import { OperationType, GraphQLTaggedNode } from 'relay-runtime'; +import { OfflineRenderProps, QueryOptionsOffline } from '../RelayOfflineTypes'; +import { useMemoOperationDescriptor } from 'relay-hooks/lib/useQuery'; +import { Environment } from '@wora/relay-offline'; + +export const useLazyLoadQueryOffline = function ( + gqlQuery: GraphQLTaggedNode, + variables: TOperationType['variables'], + options: QueryOptionsOffline = {}, +): OfflineRenderProps { + const environment = useRelayEnvironment(); + const ref = useRef<{ haveProps: boolean }>({ + haveProps: false, + }); + + const rehydrated = environment.isRehydrated(); + + const query = useMemoOperationDescriptor(gqlQuery, variables); + + const queryFetcher = useQueryFetcher(query); + + const { fetchPolicy, networkCacheConfig, ttl, skip, fetchKey, fetchObserver } = options; + + const online = environment.isOnline(); + + const { props, error, ...others } = queryFetcher.execute( + environment, + query, + { + networkCacheConfig, + fetchPolicy: rehydrated && online ? fetchPolicy : STORE_ONLY, + skip, + fetchKey, + fetchObserver, + }, + (environment, query) => environment.retain(query, { ttl }), // TODO new directive + ); + ref.current = { + haveProps: !!props, + }; + + if (!rehydrated) { + const promise = environment + .hydrate() + .then(() => undefined) + .catch((error) => { + throw error; // + }); + const { haveProps } = ref.current; + if (!haveProps) { + throw promise; + } + } + return { + ...others, + props, + rehydrated, + error: error, + online, + }; +}; diff --git a/src/hooks/useOffline.ts b/src/hooks/useOffline.ts index 7a16fc7..ac76423 100644 --- a/src/hooks/useOffline.ts +++ b/src/hooks/useOffline.ts @@ -1,13 +1,12 @@ -import { useState, useEffect, useRef, useContext } from 'react'; -import { ReactRelayContext } from 'react-relay'; -import { RelayContext } from 'relay-runtime'; +import { useState, useEffect, useRef } from 'react'; import * as areEqual from 'fbjs/lib/areEqual'; import { OfflineRecordCache } from '@wora/offline-first'; -import { Payload } from '@wora/relay-offline'; +import { Payload, Environment } from '@wora/relay-offline'; +import { useRelayEnvironment } from 'relay-hooks'; export function useOffline(): ReadonlyArray> { const ref = useRef(); - const { environment }: RelayContext = useContext(ReactRelayContext); + const environment = useRelayEnvironment(); const [state, setState] = useState>>(environment.getStoreOffline().getListMutation()); useEffect(() => { diff --git a/src/hooks/usePreloadedQueryOffline.ts b/src/hooks/usePreloadedQueryOffline.ts new file mode 100644 index 0000000..3934180 --- /dev/null +++ b/src/hooks/usePreloadedQueryOffline.ts @@ -0,0 +1,9 @@ +import { OperationType } from 'relay-runtime'; +import { OfflineLoadQuery, OfflineRenderProps } from '../RelayOfflineTypes'; +import { usePreloadedQuery } from 'relay-hooks'; + +export const usePreloadedQueryOffline = ( + loadQuery: OfflineLoadQuery, +): OfflineRenderProps => { + return usePreloadedQuery(loadQuery) as OfflineRenderProps; +}; diff --git a/src/hooks/useQueryOffline.ts b/src/hooks/useQueryOffline.ts index 5239f89..6fc2968 100644 --- a/src/hooks/useQueryOffline.ts +++ b/src/hooks/useQueryOffline.ts @@ -1,23 +1,16 @@ import { useState, useRef } from 'react'; -import { useRelayEnvironment, useQueryFetcher } from 'relay-hooks'; +import { useRelayEnvironment, useQueryFetcher, STORE_ONLY } from 'relay-hooks'; import { OperationType, GraphQLTaggedNode } from 'relay-runtime'; -import { STORE_ONLY, FetchPolicy } from 'relay-hooks/lib//RelayHooksType'; -import { CacheConfig } from 'relay-runtime'; -import { OfflineRenderProps } from '../RelayOfflineTypes'; +import { OfflineRenderProps, QueryOptionsOffline } from '../RelayOfflineTypes'; import { useMemoOperationDescriptor } from 'relay-hooks/lib/useQuery'; +import { Environment } from '@wora/relay-offline'; export const useQueryOffline = function ( gqlQuery: GraphQLTaggedNode, variables: TOperationType['variables'], - options: { - fetchPolicy?: FetchPolicy; - networkCacheConfig?: CacheConfig; - ttl?: number; - skip?: boolean; - fetchKey?: string | number; - } = {}, + options: QueryOptionsOffline = {}, ): OfflineRenderProps { - const environment = useRelayEnvironment(); + const environment = useRelayEnvironment(); const ref = useRef<{ haveProps: boolean }>({ haveProps: false, }); @@ -44,16 +37,19 @@ export const useQueryOffline = function ( const queryFetcher = useQueryFetcher(); - const { fetchPolicy, networkCacheConfig, ttl, skip, fetchKey } = options; + const { fetchPolicy, networkCacheConfig, ttl, skip, fetchKey, fetchObserver } = options; + + const online = environment.isOnline(); const { props, error, ...others } = queryFetcher.execute( environment, query, { networkCacheConfig, - fetchPolicy: rehydrated && environment.isOnline() ? fetchPolicy : STORE_ONLY, + fetchPolicy: rehydrated && online ? fetchPolicy : STORE_ONLY, skip, fetchKey, + fetchObserver, }, (environment, query) => environment.retain(query, { ttl }), // TODO new directive ); @@ -65,5 +61,6 @@ export const useQueryOffline = function ( props, rehydrated, error: error, + online, }; }; diff --git a/src/hooks/useRestore.ts b/src/hooks/useRestore.ts index 43b1d35..7432c63 100644 --- a/src/hooks/useRestore.ts +++ b/src/hooks/useRestore.ts @@ -1,6 +1,7 @@ +import { Environment } from '@wora/relay-offline'; import { useState } from 'react'; -export function useRestore(environment): boolean { +export function useRestore(environment: Environment): boolean { const [rehydratate, setRehydratate] = useState(environment.isRehydrated()); if (!rehydratate) { diff --git a/src/index.ts b/src/index.ts index da05fb9..c962fd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,8 +50,12 @@ export { export { QueryRendererOffline as QueryRenderer } from './QueryRendererOffline'; export { useRestore } from './hooks/useRestore'; +export { loadQuery, loadLazyQuery } from './runtime/loadQuery'; export { useQueryOffline as useQuery } from './hooks/useQueryOffline'; +export { usePreloadedQueryOffline as usePreloadedQuery } from './hooks/usePreloadedQueryOffline'; +export { useLazyLoadQueryOffline as useLazyLoadQuery } from './hooks/useLazyLoadQueryOffline'; export { Environment, fetchQuery } from '@wora/relay-offline'; export { Store, RecordSource } from '@wora/relay-store'; export { NetInfo } from '@wora/netinfo'; export { useNetInfo, useIsConnected } from '@wora/detect-network'; +export * from './RelayOfflineTypes'; diff --git a/src/runtime/loadQuery.ts b/src/runtime/loadQuery.ts new file mode 100644 index 0000000..b42c50e --- /dev/null +++ b/src/runtime/loadQuery.ts @@ -0,0 +1,32 @@ +import { internalLoadQuery } from 'relay-hooks/lib/loadQuery'; +import { QueryFetcher } from 'relay-hooks/lib/QueryFetcher'; +import { OperationType, OperationDescriptor } from 'relay-runtime'; +import { OfflineLoadQuery, OfflineRenderProps, QueryOptionsOffline } from '../RelayOfflineTypes'; +import { Environment } from '@wora/relay-offline'; + +const queryExecute = ( + queryFetcher: QueryFetcher, + environment: Environment, + query: OperationDescriptor, + options: QueryOptionsOffline, +): OfflineRenderProps => { + const online = environment.isOnline(); + const rehydrated = environment.isRehydrated(); + if (!online) { + options.fetchPolicy = 'store-only'; + } + const data = queryFetcher.execute(environment, query, options, (environment, query) => environment.retain(query, { ttl: options.ttl })); + return { + ...data, + online, + rehydrated, + }; +}; + +export const loadLazyQuery = (): OfflineLoadQuery => { + return internalLoadQuery(true, queryExecute) as OfflineLoadQuery; +}; + +export const loadQuery = (): OfflineLoadQuery => { + return internalLoadQuery(false, queryExecute) as OfflineLoadQuery; +};