Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions bob-esbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,60 @@ export const config: import('bob-esbuild').BobConfig = {
dirs: ['packages/*'],
},
verbose: true,
manualRewritePackageJson: {
// Keep the expo/metro-friendly condition order when the package.json is
// rewritten for publication. Otherwise the generated manifest puts the ESM
// entry first and native apps pull the web bundle.
'@gqty/react': (pkg) => {
const normalizePath = (value?: string) =>
value?.replace('./dist/', './');

const deriveNativePath = (value?: string) =>
normalizePath(value)?.replace(/(\.m?js)$/u, '.native$1');

const preserveOrder = (
entry: Record<string, unknown> | string | undefined
) => {
if (entry == null || typeof entry === 'string') return entry;

const candidate =
(entry.require as string | undefined) ??
(entry.import as string | undefined);

const nativePath = deriveNativePath(candidate);

const orderedEntries: [string, unknown][] = [];

if (nativePath) orderedEntries.push(['react-native', nativePath]);

for (const [key, value] of Object.entries(entry)) {
if (key === 'react-native') continue;
orderedEntries.push([key, value]);
}

return Object.fromEntries(orderedEntries);
};

const rewrittenExports =
pkg.exports &&
Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => [
key,
preserveOrder(value as any),
])
);

const nativeMain = deriveNativePath(pkg.main);

return nativeMain || rewrittenExports
? {
...pkg,
...(nativeMain ? { 'react-native': nativeMain } : null),
...(rewrittenExports ? { exports: rewrittenExports } : null),
}
: pkg;
},
},
esbuildPluginOptions: isCLIPackage
? {
// Check https://github.com/evanw/esbuild/issues/1146
Expand Down
12 changes: 12 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,15 @@ documentations!**

Documentation, bug reports, pull requests, and other contributions are welcomed!
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for more information.

## React Native

- The React bindings ship platform-specific entry points so Metro can resolve
React Native friendly implementations automatically; no `react-dom` dependency
is required.
- `prepareReactRender` is a no-op on native targets. Server-rendering helpers
remain web-only, and hydration defaults to cache snapshots from client
renders.
- Effects that previously relied on browser visibility and online events now
listen to `AppState` changes to refetch when the app returns to the
foreground.
11 changes: 11 additions & 0 deletions packages/react/native-types/react-native.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare module 'react-native' {
export type AppStateStatus = 'active' | 'background' | 'inactive' | 'unknown';

export const AppState: {
currentState: AppStateStatus;
addEventListener(
type: 'change',
listener: (status: AppStateStatus) => void
): { remove(): void };
};
}
25 changes: 18 additions & 7 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,26 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
"react-native": "./dist/index.native.js",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./*": {
"types": "./dist/*.d.ts",
"import": "./dist/*.mjs",
"require": "./dist/*.js"
"./client": {
"types": "./dist/client.d.ts",
"react-native": "./dist/client.native.js",
"require": "./dist/client.js",
"import": "./dist/client.mjs"
},
"./ssr/ssr": {
"types": "./dist/ssr/ssr.d.ts",
"react-native": "./dist/ssr/ssr.native.js",
"require": "./dist/ssr/ssr.js",
"import": "./dist/ssr/ssr.mjs"
}
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"react-native": "dist/index.native.js",
"typings": "dist/index.d.ts",
"files": [
"dist",
Expand Down Expand Up @@ -82,7 +91,6 @@
]
},
"dependencies": {
"@react-hookz/web": "^23.1.0",
"multidict": "^1.0.9",
"p-debounce": "^4.0.0",
"p-defer": "^3.0.0",
Expand Down Expand Up @@ -135,6 +143,9 @@
},
"graphql-ws": {
"optional": true
},
"react-dom": {
"optional": true
}
},
"engines": {
Expand Down
145 changes: 145 additions & 0 deletions packages/react/src/client.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
$meta,
type BaseGeneratedSchema,
type GQtyClient,
type RetryOptions,
} from 'gqty';
import { getActivePromises } from 'gqty/Cache/query';
import type { LegacyFetchPolicy } from './common';
import { createUseMetaState, type UseMetaState } from './meta/useMetaState';
import { createUseMutation, type UseMutation } from './mutation/useMutation';
import { createGraphqlHOC, type GraphQLHOC } from './query/hoc';
import { createPrepareQuery, type PrepareQuery } from './query/preparedQuery';
import {
createUseLazyQuery,
type LazyFetchPolicy,
type UseLazyQuery,
} from './query/useLazyQuery';
import {
createUsePaginatedQuery,
type PaginatedQueryFetchPolicy,
type UsePaginatedQuery,
} from './query/usePaginatedQuery';
import { createUseQuery, type UseQuery } from './query/useQuery';
import { createUseRefetch, type UseRefetch } from './query/useRefetch';
import {
createUseTransactionQuery,
type UseTransactionQuery,
} from './query/useTransactionQuery';
import {
createSSRHelpers,
type PrepareReactRender,
type UseHydrateCache,
} from './ssr/ssr.native';
import {
createUseSubscription,
type UseSubscription,
} from './subscription/useSubscription';
import type { ReactClientOptionsWithDefaults } from './utils';

export interface ReactClientDefaults {
initialLoadingState?: boolean;
suspense?: boolean;
lazyQuerySuspense?: boolean;
transactionQuerySuspense?: boolean;
mutationSuspense?: boolean;
preparedSuspense?: boolean;
paginatedQuerySuspense?: boolean;
transactionFetchPolicy?: LegacyFetchPolicy;
lazyFetchPolicy?: LazyFetchPolicy;
paginatedQueryFetchPolicy?: PaginatedQueryFetchPolicy;
staleWhileRevalidate?: boolean;
retry?: RetryOptions;
refetchAfterHydrate?: boolean;
}

export interface CreateReactClientOptions {
defaults?: ReactClientDefaults;
}

export interface ReactClient<TSchema extends BaseGeneratedSchema> {
useQuery: UseQuery<TSchema>;
useRefetch: UseRefetch<TSchema>;
useLazyQuery: UseLazyQuery<TSchema>;
useTransactionQuery: UseTransactionQuery<TSchema>;
usePaginatedQuery: UsePaginatedQuery<TSchema>;
useMutation: UseMutation<TSchema>;
graphql: GraphQLHOC;
state: { isLoading: boolean };
prepareReactRender: PrepareReactRender;
useHydrateCache: UseHydrateCache;
useMetaState: UseMetaState;
useSubscription: UseSubscription<TSchema>;
prepareQuery: PrepareQuery<TSchema>;
}

export function createReactClient<TSchema extends BaseGeneratedSchema>(
client: GQtyClient<TSchema>,
{
defaults: { suspense = false } = {},
defaults: {
initialLoadingState = false,
transactionFetchPolicy = 'cache-first',
lazyFetchPolicy = 'network-only',
staleWhileRevalidate = false,
retry = true,
lazyQuerySuspense = false,
transactionQuerySuspense = suspense,
mutationSuspense = false,
preparedSuspense = suspense,
refetchAfterHydrate = false,
paginatedQueryFetchPolicy = 'cache-first',
paginatedQuerySuspense = suspense,
} = {},
...options
}: CreateReactClientOptions = {}
): ReactClient<TSchema> {
const opts: ReactClientOptionsWithDefaults = {
...options,
defaults: {
initialLoadingState,
lazyFetchPolicy,
lazyQuerySuspense,
mutationSuspense,
paginatedQueryFetchPolicy,
paginatedQuerySuspense,
preparedSuspense,
refetchAfterHydrate,
retry,
staleWhileRevalidate,
suspense,
transactionFetchPolicy,
transactionQuerySuspense,
},
};

const { prepareReactRender, useHydrateCache } = createSSRHelpers(
client,
opts
);

const useQuery = createUseQuery<TSchema>(client, opts);

return {
useQuery,
useRefetch: createUseRefetch(client, opts),
useLazyQuery: createUseLazyQuery<TSchema>(client, opts),
useTransactionQuery: createUseTransactionQuery<TSchema>(useQuery, opts),
usePaginatedQuery: createUsePaginatedQuery<TSchema>(client, opts),
useMutation: createUseMutation<TSchema>(client, opts),
graphql: createGraphqlHOC(client, opts),
state: {
get isLoading() {
const cache = $meta(client.schema.query)?.context.cache;
const promises = cache && getActivePromises(cache);

return !!promises?.length;
},
},
prepareReactRender,
useHydrateCache,
useMetaState: createUseMetaState(),
useSubscription: createUseSubscription<TSchema>(client),
prepareQuery: createPrepareQuery<TSchema>(client, opts),
};
}
4 changes: 3 additions & 1 deletion packages/react/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
} from 'gqty';
import * as React from 'react';

export const IS_BROWSER = typeof window !== 'undefined';
export const IS_BROWSER =
typeof window !== 'undefined' ||
globalThis.navigator?.product === 'ReactNative';

export const useIsomorphicLayoutEffect = IS_BROWSER
? React.useLayoutEffect
Expand Down
Loading