Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimentation: query optimization #2

Merged
merged 5 commits into from
Mar 25, 2024
Merged
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
62 changes: 62 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const path = require("path");

module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["react", "react-hooks"],

extends: [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
],

parserOptions: {
sourceType: "module",
project: path.resolve(__dirname + "/tsconfig.json"),
},

env: {
browser: true,
es2022: true,
},

overrides: [
{
files: ["**/__{mocks,tests}__/**/*.{ts,tsx}"],
rules: {
"no-empty": ["error", { allowEmptyCatch: true }],
},
},
{
files: ["*.d.ts"],
rules: {
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/no-unused-vars": "off",
},
},
{
files: ["clients/**/src/graphql/**/*.{ts,tsx}"],
rules: {
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-explicit-any": "off",
},
},
],

rules: {
"no-implicit-coercion": "error",
"no-param-reassign": "error",
"no-var": "error",
"object-shorthand": "warn",
"prefer-const": "error",

"no-extra-boolean-cast": "off",

"react/jsx-boolean-value": ["error", "always"],

"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},

ignorePatterns: [".eslintrc.js"],
};
1 change: 1 addition & 0 deletions docs/docs/use-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ sidebar_label: useQuery
- `variables`: your query variables
- `config` (optional)
- `suspense`: use React Suspense (default: `false`)
- `optimize`: adapt query to only require data that's missing from the cache (default: `false`)

### Returns

Expand Down
53 changes: 32 additions & 21 deletions example/components/AccountMembership.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { FragmentOf, readFragment } from "gql.tada";
import { useState } from "react";
import { P, match } from "ts-pattern";
import { graphql } from "../graphql";
import { AccountMembershipDetail } from "./AccountMembershipDetail";

export const accountMembershipFragment = graphql(`
fragment AccountMembership on AccountMembership {
Expand All @@ -12,7 +14,6 @@ export const accountMembershipFragment = graphql(`
}
statusInfo {
__typename
status
... on AccountMembershipBindingUserErrorStatusInfo {
restrictedTo {
firstName
Expand All @@ -36,27 +37,37 @@ type Props = {
export const AccountMembership = ({ data }: Props) => {
const accountMembership = readFragment(accountMembershipFragment, data);

const [isOpen, setIsOpen] = useState(false);

return (
<div className="AccountMembership">
<strong>{accountMembership.id}</strong>:
{match(accountMembership)
.with(
{ user: { firstName: P.string, lastName: P.string } },
({ user: { firstName, lastName } }) => `${firstName} ${lastName}`,
)
.with(
{
statusInfo: {
restrictedTo: { firstName: P.string, lastName: P.string },
},
},
({
statusInfo: {
restrictedTo: { firstName, lastName },
<>
<div className="AccountMembership" onClick={() => setIsOpen(true)}>
<strong>{accountMembership.id}</strong>:
{match(accountMembership)
.with(
{ user: { firstName: P.string, lastName: P.string } },
({ user: { firstName, lastName } }) => `${firstName} ${lastName}`,
)
.with(
{
statusInfo: {
restrictedTo: { firstName: P.string, lastName: P.string },
},
},
}) => `${firstName} ${lastName} (restricted to)`,
)
.otherwise(() => "No user")}
</div>
({
statusInfo: {
restrictedTo: { firstName, lastName },
},
}) => `${firstName} ${lastName} (restricted to)`,
)
.otherwise(() => "No user")}
</div>
{isOpen ? (
<AccountMembershipDetail
accountMembershipId={accountMembership.id}
onPressClose={() => setIsOpen(false)}
/>
) : null}
</>
);
};
70 changes: 70 additions & 0 deletions example/components/AccountMembershipDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { AsyncData, Result } from "@swan-io/boxed";
import { useState } from "react";
import { P, match } from "ts-pattern";
import { ClientError, useQuery } from "../../src";
import { graphql } from "../graphql";
import { UserCard } from "./UserCard";

export const accountMembershipDetailQuery = graphql(`
query AccountMembershipDetail($accountMembershipId: ID!) {
accountMembership(id: $accountMembershipId) {
id
user {
id
firstName
lastName
}
}
}
`);

type Props = {
accountMembershipId: string;
onPressClose: () => void;
};

export const AccountMembershipDetail = ({
accountMembershipId,
onPressClose,
}: Props) => {
const [data] = useQuery(
accountMembershipDetailQuery,
{ accountMembershipId },
{ optimize: true },
);

const [showDetails, setShowDetails] = useState(false);

return match(data)
.with(AsyncData.P.NotAsked, () => "Nothing")
.with(AsyncData.P.Loading, () => "Loading")
.with(AsyncData.P.Done(Result.P.Error(P.select())), (error) => {
ClientError.forEach(error, (error) => console.error(error));
return "Error";
})
.with(
AsyncData.P.Done(Result.P.Ok(P.select())),
({ accountMembership }) => {
if (accountMembership == null) {
return <div>No membership</div>;
}
return (
<dialog open={true}>
<button onClick={onPressClose}>Close</button>
<div>
<strong>Membership: {accountMembership.id}</strong>

{showDetails ? (
<UserCard accountMembershipId={accountMembership.id} />
) : (
<button onClick={() => setShowDetails(true)}>
Show user info
</button>
)}
</div>
</dialog>
);
},
)
.exhaustive();
};
11 changes: 10 additions & 1 deletion example/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ export const App = () => {
"fa3a2485-43bc-461e-b38c-5a9bc3750c7d",
);
const [suspense, setSuspense] = useState(false);
const [optimize, setOptimize] = useState(false);

const [data, { isLoading, reload, refresh }] = useQuery(
transactionListQuery,
{
accountMembershipId,
after,
},
{ suspense },
{ suspense, optimize },
);

const toggleAccountMembership = useCallback(() => {
Expand Down Expand Up @@ -65,6 +66,14 @@ export const App = () => {
/>
Suspense
</label>
<label>
<input
type="checkbox"
checked={optimize}
onChange={() => setOptimize((x) => !x)}
/>
Optimize
</label>
</header>

{match(data)
Expand Down
80 changes: 80 additions & 0 deletions example/components/UserCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { AsyncData, Result } from "@swan-io/boxed";
import { P, match } from "ts-pattern";
import { ClientError, useQuery } from "../../src";
import { graphql } from "../graphql";

export const accountMembershipUserDetailQuery = graphql(`
query AccountMembershipUserDetail($accountMembershipId: ID!) {
accountMembership(id: $accountMembershipId) {
id
user {
id
firstName
lastName
birthDate
mobilePhoneNumber
}
}
}
`);

type Props = {
accountMembershipId: string;
};

const formatter = new Intl.DateTimeFormat();

export const UserCard = ({ accountMembershipId }: Props) => {
const [data] = useQuery(
accountMembershipUserDetailQuery,
{
accountMembershipId,
},
{ optimize: true },
);

return match(data)
.with(AsyncData.P.NotAsked, () => "Nothing")
.with(AsyncData.P.Loading, () => "Loading")
.with(AsyncData.P.Done(Result.P.Error(P.select())), (error) => {
ClientError.forEach(error, (error) => console.error(error));
return "Error";
})
.with(
AsyncData.P.Done(Result.P.Ok(P.select())),
({ accountMembership }) => {
if (accountMembership == null) {
return null;
}
const user = accountMembership.user;
if (user == null) {
return <div>No user</div>;
}
return (
<div className="User">
<ul>
<li>
<strong>ID:</strong> {user.id}
</li>
<li>
<strong>First name:</strong> {user.firstName}
</li>
<li>
<strong>Last name:</strong> {user.lastName}
</li>
{user.birthDate != null ? (
<li>
<strong>Birthdate:</strong>{" "}
{formatter.format(new Date(user.birthDate))}
</li>
) : null}
<li>
<strong>Mobile phone number:</strong> {user.mobilePhoneNumber}
</li>
</ul>
</div>
);
},
)
.exhaustive();
};
2 changes: 2 additions & 0 deletions example/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ export const graphql = initGraphQLTada<{
ID: string;
Currency: string;
AmountValue: string;
Date: string;
PhoneNumber: string;
};
}>();
6 changes: 6 additions & 0 deletions src/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ export class ClientCache {
});
}

getFromCacheWithoutKey(cacheKey: symbol) {
return this.get(cacheKey).flatMap((entry) => {
return Option.Some(entry.value);
});
}

get(cacheKey: symbol): Option<CacheEntry> {
if (this.cache.has(cacheKey)) {
return Option.Some(this.cache.get(cacheKey) as CacheEntry);
Expand Down
Loading