Skip to content

Commit

Permalink
fix(react): reduce over-fetching between renders #1594
Browse files Browse the repository at this point in the history
  • Loading branch information
vicary committed Aug 5, 2023
1 parent 0d21106 commit ddc9f72
Show file tree
Hide file tree
Showing 26 changed files with 193 additions and 96 deletions.
43 changes: 0 additions & 43 deletions examples/gnt/app/MyComponent.tsx

This file was deleted.

File renamed without changes.
File renamed without changes.
68 changes: 68 additions & 0 deletions examples/gnt/app/components/CscCharacterSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import type { Variables } from 'gqty';
import { useState, type FunctionComponent } from 'react';
import Button from '~/components/tailwindui/Button';
import type { Query } from '~/gqty';
import { useQuery } from '~/gqty/react';
import Avatar from './Avatar';
import Card from './Card';
import SmallText from './SmallText';
import Text from './Text';

export type Props = {};

const Component: FunctionComponent<Props> = () => {
const [searchValue, setSearchValue] = useState<string>();

return (
<>
<SelectBox onChange={setSearchValue} />
{searchValue && <Character id={searchValue} />}
</>
);
};

const SelectBox: FunctionComponent<{
onChange?: (value: string) => void;
}> = ({ onChange }) => {
const [value, setValue] = useState<string>();

return (
<div className="flex gap-3">
<input
type="number"
defaultValue={value}
onChange={(e) => setValue(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 w-full text-black"
/>
<Button
onClick={() => {
if (value) {
onChange?.(value);
}
}}
>
Search
</Button>
</div>
);
};

const Character: FunctionComponent<Variables<Query['character']>> = (props) => {
const character = useQuery().character(props);

return (
<Card>
<Avatar character={character} />

<div className="flex-1">
<Text>{character?.name}</Text>
<SmallText>{character?.species}</SmallText>
<SmallText>{character?.origin?.name}</SmallText>
</div>
</Card>
);
};

export default Component;
66 changes: 66 additions & 0 deletions examples/gnt/app/components/CscCharactersSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client';

import type { Variables } from 'gqty';
import { useState, type FunctionComponent } from 'react';
import Button from '~/components/tailwindui/Button';
import type { Query } from '~/gqty';
import { useQuery } from '~/gqty/react';
import Avatar from './Avatar';
import Card from './Card';
import SmallText from './SmallText';
import Text from './Text';

export type Props = {};

const Component: FunctionComponent<Props> = () => {
const [searchValue, setSearchValue] = useState<string>();

return (
<>
<SearchBox onChange={setSearchValue} />
<Characters filter={{ name: searchValue }} />
</>
);
};

const SearchBox: FunctionComponent<{
onChange?: (value: string) => void;
}> = ({ onChange }) => {
const [inputName, setInputName] = useState('');

return (
<div className="flex gap-3">
<input
type="text"
defaultValue={inputName}
onChange={(e) => setInputName(e.target.value)}
className="border border-gray-300 rounded-md px-3 py-2 w-full text-black"
/>
<Button onClick={() => onChange?.(inputName)}>Search</Button>
</div>
);
};

const Characters: FunctionComponent<Variables<Query['characters']>> = (
props
) => {
const query = useQuery();

return (
<>
{query.characters(props)?.results?.map((character) => (
<Card key={character?.id ?? '0'}>
<Avatar character={character} />

<div className="flex-1">
<Text>{character?.name}</Text>
<SmallText>{character?.species}</SmallText>
<SmallText>{character?.origin?.name}</SmallText>
</div>
</Card>
))}
</>
);
};

export default Component;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolve, type Query } from '../gqty';
import { resolve, type Query } from '~/gqty';

/** RSC */
export default async function Character({
export default async function RscCharacter({
id,
}: Parameters<Query['character']>[0]) {
const data = await resolve(({ query }) => {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 7 additions & 3 deletions examples/gnt/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Suspense } from 'react';
import Character from './Character';
import MyComponent from './MyComponent';
import CharacterSearch from './components/CscCharacterSearch';
import CharactersSearch from './components/CscCharactersSearch';
import Character from './components/RscCharacter';

CharacterSearch;
CharactersSearch;

export default function Home() {
return (
<main className="p-5 min-h-screen">
{/* CSR test */}

<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
<CharactersSearch />
</Suspense>

{/* RSC test */}
Expand Down
7 changes: 4 additions & 3 deletions examples/gnt/gqty/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* GQty: You can safely modify this file based on your needs.
*/

import { createLogger } from '@gqty/logger';
import { Cache, GQtyError, createClient, type QueryFetcher } from 'gqty';
import {
generatedSchema,
Expand All @@ -13,8 +14,6 @@ const queryFetcher: QueryFetcher = async function (
{ query, variables, operationName, extensions },
fetchOptions
) {
console.debug({ query, variables, operationName, ...extensions });

// Modify "https://rickandmortyapi.com/graphql" if needed
const response = await fetch('https://rickandmortyapi.com/graphql', {
method: 'POST',
Expand All @@ -32,7 +31,7 @@ const queryFetcher: QueryFetcher = async function (

if (response.status >= 400) {
throw new GQtyError(
`GraphQL endpoint responded with HTTP ${response.status}: ${response.statusText}.`
`GraphQL endpoint responded with HTTP status ${response.status}.`
);
}

Expand Down Expand Up @@ -71,6 +70,8 @@ export const client = createClient<GeneratedSchema>({
},
});

createLogger(client).start();

// Core functions
export const { resolve, subscribe, schema } = client;

Expand Down
3 changes: 2 additions & 1 deletion examples/gnt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"eslint-config-next": "13.3.0",
"postcss": "8.4.21",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"utf-8-validate": "^5.0.2"
}
}
2 changes: 1 addition & 1 deletion internal/test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"build": "bob-ts -i src -f interop",
"prepare": "pnpm build",
"start": "nodemon --exec \"concurrently pnpm:build tsc\" -w src/index.ts",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --config local.jest.config.js"
"test": "jest --config local.jest.config.js"
},
"dependencies": {
"@graphql-ez/fastify": "^0.12.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"build": "bob-tsm build.ts",
"prepare": "bob-tsm build.ts",
"postpublish": "gh-release",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
"test": "jest",
"test:watch": "jest --watch",
"test:watch-coverage": "rimraf coverage && mkdirp coverage/lcov-report && concurrently --raw \"jest --watchAll\" \"serve -l 8787 coverage/lcov-report\" \"wait-on tcp:8787 coverage/lcov-report/index.html && open-cli http://localhost:8787\""
},
"dependencies": {
Expand Down
6 changes: 3 additions & 3 deletions packages/gqty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
"postpublish": "gh-release",
"size": "size-limit",
"start": "bob-esbuild watch",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"test:specific": "NODE_OPTIONS=--experimental-vm-modules jest test/interfaces-unions.test.ts --watch --no-coverage -u",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
"test": "jest",
"test:specific": "jest test/interfaces-unions.test.ts --watch --no-coverage -u",
"test:watch": "jest --watch",
"test:watch-coverage": "rimraf coverage && mkdirp coverage/lcov-report && concurrently --raw \"jest --watchAll\" \"serve -l 8787 coverage/lcov-report\" \"wait-on tcp:8787 coverage/lcov-report/index.html && open-cli http://localhost:8787\""
},
"dependencies": {
Expand Down
8 changes: 4 additions & 4 deletions packages/gqty/src/Client/batching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const pendingSelections = new Map<Cache, Map<string, Set<Set<Selection>>>>();
export const addSelections = (
cache: Cache,
key: string,
value: Set<Selection>
selections: Set<Selection>
) => {
if (!pendingSelections.has(cache)) {
pendingSelections.set(cache, new Map());
Expand All @@ -18,19 +18,19 @@ export const addSelections = (
selectionsByKey.set(key, new Set());
}

return selectionsByKey.get(key)!.add(value);
return selectionsByKey.get(key)!.add(selections);
};

export const getSelectionsSet = (cache: Cache, key: string) =>
pendingSelections.get(cache)?.get(key);

export const delSelectionsSet = (cache: Cache, key: string) =>
export const delSelectionSet = (cache: Cache, key: string) =>
pendingSelections.get(cache)?.delete(key) ?? false;

export const popSelectionsSet = (cache: Cache, key: string) => {
const result = getSelectionsSet(cache, key);

delSelectionsSet(cache, key);
delSelectionSet(cache, key);

return result;
};
9 changes: 5 additions & 4 deletions packages/gqty/src/Client/debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@ export type DebugEvent = {
export type DebugEventListener = (event: DebugEvent) => void;

export type Debugger = {
dispatch: (event: DebugEvent) => void;
dispatch: (event: DebugEvent) => Promise<void>;

/** Returns an unsubscribe function */
subscribe: (listener: DebugEventListener) => () => void;
};

export const createDebugger = () => {
export const createDebugger = (): Debugger => {
const subs = new Set<DebugEventListener>();

return {
dispatch: (event: DebugEvent) => {
subs.forEach((sub) => sub(event));
dispatch: async (event: DebugEvent) => {
await Promise.all([...subs].map((sub) => sub(event)));
},

subscribe: (listener: DebugEventListener) => {
subs.add(listener);
return () => subs.delete(listener);
Expand Down
2 changes: 1 addition & 1 deletion packages/gqty/src/Client/resolveSelections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const fetchSelections = <
}

// TODO: Defer logging until after cache update
debug?.dispatch({
await debug?.dispatch({
cache,
request: queryPayload,
result,
Expand Down
10 changes: 3 additions & 7 deletions packages/gqty/src/Client/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type Cache } from '../Cache';
import { type GQtyError, type RetryOptions } from '../Error';
import { type ScalarsEnumsHash, type Schema } from '../Schema';
import { type Selection } from '../Selection';
import { addSelections, delSelectionsSet, getSelectionsSet } from './batching';
import { addSelections, delSelectionSet, getSelectionsSet } from './batching';
import { createContext, type SchemaContext } from './context';
import { type Debugger } from './debugger';
import {
Expand Down Expand Up @@ -251,17 +251,13 @@ export const createResolvers = <TSchema extends BaseGeneratedSchema>({

pendingQueries.delete(pendingSelections);

delSelectionsSet(clientCache, selectionsCacheKey);
delSelectionSet(clientCache, selectionsCacheKey);

return fetchSelections(selections, {
cache: context.cache,
debugger: debug,
extensions,
fetchOptions: {
...fetchOptions,
cachePolicy,
retryPolicy,
},
fetchOptions: { ...fetchOptions, cachePolicy, retryPolicy },
operationName,
}).then((results) => {
updateCaches(
Expand Down
4 changes: 2 additions & 2 deletions packages/logger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
"postpublish": "gh-release",
"size": "size-limit",
"start": "bob-esbuild watch",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"prettier": "^3.0.1"
Expand Down
Loading

0 comments on commit ddc9f72

Please sign in to comment.