Skip to content
26 changes: 25 additions & 1 deletion docs/how-to/use-multiple-queries-in-a-template/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,33 @@ export default function Component(props) {
> [!note] Learn More
> The [getting started example](https://github.com/wpengine/faustjs/tree/canary/examples/next/faustwp-getting-started/wp-templates) uses `Component.queries` with a shared layout query in each of its templates.

## 4. Run the same query with different variables

To list a query more than once, for example one section of posts per category, give each entry its own variables and pass the same variables to `useFaustQuery`:

```js title="wp-templates/front-page.js"
import { useFaustQuery } from "@faustwp/core";
import { GET_POSTS_QUERY } from "../queries/GetPosts";

export default function Component() {
const { posts: sports } =
useFaustQuery(GET_POSTS_QUERY, { variables: { category: "sports" } }) ?? {};
const { posts: news } =
useFaustQuery(GET_POSTS_QUERY, { variables: { category: "news" } }) ?? {};

// ...
}

Component.queries = [
{ query: GET_POSTS_QUERY, variables: () => ({ category: "sports" }) },
{ query: GET_POSTS_QUERY, variables: () => ({ category: "news" }) },
];
```

The variables must match what the entry's `variables` function returns, in any key order. Since the queries run again in the browser, where the function doesn't receive `extra`, don't base these variables on `extra`. Without the second argument, `useFaustQuery` returns the last entry's result.

## Notes and limitations

- **Use one or the other.** A template can define `Component.query` or `Component.queries`, not both. Faust throws ``Only either `Component.query` or `Component.queries` can be provided, but not both.`` if it finds both.
- **Results come from `useFaustQuery`, not `props.data`.** `props.data` is only filled in for `Component.query`.
- **Queries run again in the browser.** After the page loads, Faust runs the queries again in the browser. On previews, they only run in the browser. In the browser, the `variables` functions receive the seed node, `asPreview` and `locale`, but not `extra`.
- **Each query can only be listed once.** Faust stores results by the query's text, so listing the same query twice with different variables keeps only the last result. To fetch the same data with different variables, define a second query with a different operation name. See [#1742](https://github.com/wpengine/faustjs/issues/1742).
32 changes: 12 additions & 20 deletions packages/faustwp-core/src/components/WordPressTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { QueryOptions } from '@apollo/client';
// eslint-disable-next-line import/extensions
import { print } from '@apollo/client/utilities';
import { sha256 } from 'js-sha256';
import { useRouter } from 'next/router.js';
import React, {
PropsWithChildren,
Expand All @@ -24,6 +21,7 @@ import {
getPreviewSeedNode,
} from '../queries/seedQuery.js';
import { FaustContext, FaustQueries } from '../store/FaustContext.js';
import { storeFaustQueries } from '../store/faustQueries.js';
import { getQueryParam } from '../utils/convert.js';
import { getBodyClasses } from '../utils/getBodyClasses.js';
import { isWordPressPreview } from '../utils/isWordPressPreview.js';
Expand Down Expand Up @@ -155,29 +153,23 @@ export function WordPressTemplateInternal(
return;
}

let queries: FaustQueries | null = null;

const queryCalls = template.queries.map(({ query, variables }) => {
const queryVariables = variables
? variables(seedNode, { asPreview: isPreview, locale })
: undefined;
return client.query({
query,
variables: queryVariables,
});
});

const queriesRes = await Promise.all(queryCalls);

queries = {};

queriesRes.forEach((queryRes, index) => {
if (queries && template.queries) {
queries[sha256(print(template.queries[index].query))] = queryRes.data;
}
return client
.query({
query,
variables: queryVariables,
})
.then((result) => ({
Comment thread
josephfusco marked this conversation as resolved.
query,
variables: queryVariables,
data: result.data,
}));
});

setQueries(queries);
setQueries(storeFaustQueries(await Promise.all(queryCalls)));

setLoading(false);
})();
Expand Down
23 changes: 8 additions & 15 deletions packages/faustwp-core/src/getWordPressProps.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// eslint-disable-next-line import/extensions
import { print } from '@apollo/client/utilities';
import type { DocumentNode } from 'graphql';
import { sha256 } from 'js-sha256';
import { GetServerSidePropsContext, GetStaticPropsContext } from 'next';
import { addApolloState, getApolloClient } from './client.js';
import { FaustTemplateProps } from './components/WordPressTemplate.js';
Expand All @@ -16,6 +13,7 @@ import { SEED_QUERY, SeedNode } from './queries/seedQuery.js';
import { debugLog, infoLog } from './utils/log.js';
import { hooks } from './wpHooks/index.js';
import { FaustQueries } from './store/FaustContext.js';
import { storeFaustQueries } from './store/faustQueries.js';

export const DEFAULT_ISR_REVALIDATE = 60 * 15; // 15 minutes

Expand Down Expand Up @@ -195,20 +193,15 @@ export async function getWordPressProps(
extra,
)
: undefined;
return client.query({
query,
variables: queryVariables,
});
return client
.query({
query,
variables: queryVariables,
})
.then(({ data }) => ({ query, variables: queryVariables, data }));
});
const queriesRes = await Promise.all(queryCalls);

queries = {};

queriesRes.forEach((queryRes, index) => {
if (queries && template.queries) {
queries[sha256(print(template.queries[index].query))] = queryRes.data;
}
});
queries = storeFaustQueries(await Promise.all(queryCalls));
}

const appProps = addApolloState(client, {
Expand Down
23 changes: 15 additions & 8 deletions packages/faustwp-core/src/hooks/useFaustContext.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { DocumentNode } from '@apollo/client';
// eslint-disable-next-line import/extensions
import { print } from '@apollo/client/utilities';
import { sha256 } from 'js-sha256';
import { useContext } from 'react';
import { FaustContext } from '../store/FaustContext.js';
import { readFaustQuery } from '../store/faustQueries.js';

export function useFaustQuery<TData>(query: DocumentNode): TData {
export type UseFaustQueryOptions = {
/**
* The variables the query was run with in `Component.queries`. Pass them
* when a template runs the same query more than once with different
* variables, to get the result for these variables.
*/
variables?: { [key: string]: any };
};

export function useFaustQuery<TData>(
query: DocumentNode,
options?: UseFaustQueryOptions,
): TData {
const context = useContext(FaustContext);

if (context === undefined) {
throw new Error('useFaustQuery must be used within a FaustProvider');
}

const sha = sha256(print(query));

// eslint-disable-next-line no-underscore-dangle
return context?.queries?.[sha] as TData;
return readFaustQuery(context?.queries, query, options?.variables) as TData;
}
6 changes: 5 additions & 1 deletion packages/faustwp-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import { useLogin } from './hooks/useLogin.js';
import { useBodyClass } from './hooks/useBodyClass.js';
import { getBodyClasses } from './utils/getBodyClasses.js';
import { useLogout } from './hooks/useLogout.js';
import { useFaustQuery } from './hooks/useFaustContext.js';
import {
useFaustQuery,
UseFaustQueryOptions,
} from './hooks/useFaustContext.js';
import { FaustContext } from './store/FaustContext.js';

import {
Expand Down Expand Up @@ -90,5 +93,6 @@ export {
FaustPage,
hooks,
useFaustQuery,
UseFaustQueryOptions,
FaustContext,
};
130 changes: 130 additions & 0 deletions packages/faustwp-core/src/store/faustQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { DocumentNode } from '@apollo/client';
// eslint-disable-next-line import/extensions
import { print } from '@apollo/client/utilities';
import { sha256 } from 'js-sha256';
import type { FaustQueries } from './FaustContext.js';

type Variables = { [key: string]: any };

// Suffix for the entry that records which variables the query-only key's
// result was fetched with.
const VARIABLES_SUFFIX = ':variables';

function sortKeys(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(sortKeys);
}

if (value !== null && typeof value === 'object') {
return Object.keys(value)
.sort()
.reduce<Variables>((sorted, key) => {
// eslint-disable-next-line no-param-reassign
sorted[key] = sortKeys((value as Variables)[key]);
return sorted;
}, {});
}

return value;
}

/**
* Get the key a query's result is stored under. With variables, the key also
* covers them, in any key order, so the same query can be stored once per set
* of variables. Variables go through JSON first, as they do when Apollo sends
* them, so values like dates keep what makes them different.
*/
export function getFaustQueryKey(
query: DocumentNode,
variables?: Variables,
): string {
const printed = print(query);

return variables === undefined
? sha256(printed)
: sha256(
`${printed}\n${JSON.stringify(
sortKeys(JSON.parse(JSON.stringify(variables))),
)}`,
);
}

/**
* Store the results of a template's `queries`.
*
* Each result is stored once. The query-only key holds the last result for
* each query, as it always has, so `useFaustQuery(query)` keeps working.
* Results with variables that aren't under the query-only key are stored
* under a key for the query and its variables.
*/
export function storeFaustQueries(
results: {
query: DocumentNode;
variables?: Variables;
data: any;
}[],
): FaustQueries {
const queries: FaustQueries = {};
const entries = results.map(({ query, variables, data }) => ({
queryKey: getFaustQueryKey(query),
variablesKey:
variables === undefined ? undefined : getFaustQueryKey(query, variables),
data,
}));
const lastIndex = new Map<string, number>();

// A later entry for the same query and variables replaces an earlier one.
entries.forEach(({ queryKey, variablesKey }, index) => {
lastIndex.set(queryKey, index);
if (variablesKey) {
lastIndex.set(variablesKey, index);
}
});

entries.forEach(({ queryKey, variablesKey, data }, index) => {
if (lastIndex.get(queryKey) === index) {
queries[queryKey] = data;

if (variablesKey) {
queries[queryKey + VARIABLES_SUFFIX] = variablesKey;
}
}
});

entries.forEach(({ queryKey, variablesKey, data }, index) => {
if (
variablesKey &&
lastIndex.get(variablesKey) === index &&
queries[queryKey + VARIABLES_SUFFIX] !== variablesKey
) {
queries[variablesKey] = data;
}
});

return queries;
}

/**
* Read a stored query result, optionally the one for specific variables.
*/
export function readFaustQuery(
queries: FaustQueries | null | undefined,
query: DocumentNode,
variables?: Variables,
): any {
const queryKey = getFaustQueryKey(query);

if (variables === undefined) {
return queries?.[queryKey];
}

const variablesKey = getFaustQueryKey(query, variables);

if (queries?.[variablesKey] !== undefined) {
return queries[variablesKey];
}

return queries?.[queryKey + VARIABLES_SUFFIX] === variablesKey
? queries[queryKey]
: undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -308,21 +308,22 @@ xdescribe('<WordPressTemplateInternal />', () => {
} as any as ApolloClient<NormalizedCacheObject>),
);

clientQueryMock.mockImplementationOnce(() => ({
// client.query() returns a promise, like Apollo's.
clientQueryMock.mockResolvedValueOnce({
data: {
generalSettings: {
title: 'testing',
},
},
}));
});

clientQueryMock.mockImplementationOnce(() => ({
clientQueryMock.mockResolvedValueOnce({
data: {
post: {
databaseId: '50',
},
},
}));
});

await act(async () => {
render(
Expand Down
Loading
Loading