From 1d263fc3a2e5d06a06e8d5a881e08e57c7999023 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 27 Aug 2023 15:21:24 +0700 Subject: [PATCH 01/20] Move Cell things into a folder --- .../{ => cell}/CellCacheContext.tsx | 0 .../{ => cell}/CellErrorBoundary.tsx | 2 +- .../web/src/components/cell/cellTypes.tsx | 181 +++++++++++++++++ .../components/{ => cell}/createCell.test.tsx | 0 .../src/components/{ => cell}/createCell.tsx | 187 +----------------- .../components/cell/createSuspendingCell.tsx | 0 packages/web/src/index.ts | 7 +- 7 files changed, 190 insertions(+), 187 deletions(-) rename packages/web/src/components/{ => cell}/CellCacheContext.tsx (100%) rename packages/web/src/components/{ => cell}/CellErrorBoundary.tsx (95%) create mode 100644 packages/web/src/components/cell/cellTypes.tsx rename packages/web/src/components/{ => cell}/createCell.test.tsx (100%) rename packages/web/src/components/{ => cell}/createCell.tsx (57%) create mode 100644 packages/web/src/components/cell/createSuspendingCell.tsx diff --git a/packages/web/src/components/CellCacheContext.tsx b/packages/web/src/components/cell/CellCacheContext.tsx similarity index 100% rename from packages/web/src/components/CellCacheContext.tsx rename to packages/web/src/components/cell/CellCacheContext.tsx diff --git a/packages/web/src/components/CellErrorBoundary.tsx b/packages/web/src/components/cell/CellErrorBoundary.tsx similarity index 95% rename from packages/web/src/components/CellErrorBoundary.tsx rename to packages/web/src/components/cell/CellErrorBoundary.tsx index 95a671f0dcfa..20316cac1781 100644 --- a/packages/web/src/components/CellErrorBoundary.tsx +++ b/packages/web/src/components/cell/CellErrorBoundary.tsx @@ -1,6 +1,6 @@ import React from 'react' -import type { CellFailureProps } from './createCell' +import type { CellFailureProps } from './cellTypes' type CellErrorBoundaryProps = { // Note that the fallback has to be an FC, not a Node diff --git a/packages/web/src/components/cell/cellTypes.tsx b/packages/web/src/components/cell/cellTypes.tsx new file mode 100644 index 000000000000..400302a38005 --- /dev/null +++ b/packages/web/src/components/cell/cellTypes.tsx @@ -0,0 +1,181 @@ +import { ComponentProps, JSXElementConstructor } from 'react' + +import { OperationVariables } from '@apollo/client' +import type { DocumentNode } from 'graphql' +import type { A } from 'ts-toolbelt' + +/** + * + * If the Cell has a `beforeQuery` function, then the variables are not required, + * but instead the arguments of the `beforeQuery` function are required. + * + * If the Cell does not have a `beforeQuery` function, then the variables are required. + * + * Note that a query that doesn't take any variables is defined as {[x: string]: never} + * The ternary at the end makes sure we don't include it, otherwise it won't allow merging any + * other custom props from the Success component. + * + */ +type CellPropsVariables = Cell extends { + beforeQuery: (...args: any[]) => any +} + ? Parameters[0] extends unknown + ? Record + : Parameters[0] + : GQLVariables extends Record + ? unknown + : GQLVariables +/** + * Cell component props which is the combination of query variables and Success props. + */ + +export type CellProps< + CellSuccess extends keyof JSX.IntrinsicElements | JSXElementConstructor, + GQLResult, + CellType, + GQLVariables +> = A.Compute< + Omit< + ComponentProps, + | keyof CellPropsVariables + | keyof GQLResult + | 'updating' + | 'queryResult' + > & + CellPropsVariables +> + +export type CellLoadingProps = { + queryResult?: Partial< + Omit, 'loading' | 'error' | 'data'> + > +} + +export type CellFailureProps = { + queryResult?: Partial< + Omit, 'loading' | 'error' | 'data'> + > + error?: QueryOperationResult['error'] | Error // for tests and storybook + + /** + * @see {@link https://www.apollographql.com/docs/apollo-server/data/errors/#error-codes} + */ + errorCode?: string + updating?: boolean +} +// aka guarantee that all properties in T exist +// This is necessary for Cells, because if it doesn't exist it'll go to Empty or Failure +type Guaranteed = { + [K in keyof T]-?: NonNullable +} +/** + * Use this type, if you are forwarding on the data from your Cell's Success component + * Because Cells automatically checks for "empty", or "errors" - if you receive the data type in your + * Success component, it means the data is guaranteed (and non-optional) + * + * @params TData = Type of data based on your graphql query. This can be imported from 'types/graphql' + * @example + * import type {FindPosts} from 'types/graphql' + * + * const { post } = CellSuccessData + * + * post.id // post is non optional, so no need to do post?.id + * + */ + +export type CellSuccessData = Omit, '__typename'> +/** + * @MARK not sure about this partial, but we need to do this for tests and storybook. + * + * `updating` is just `loading` renamed; since Cells default to stale-while-refetch, + * this prop lets users render something like a spinner to show that a request is in-flight. + */ + +export type CellSuccessProps< + TData = any, + TVariables extends OperationVariables = any +> = { + queryResult?: Partial< + Omit, 'loading' | 'error' | 'data'> + > + updating?: boolean +} & A.Compute> // pre-computing makes the types more readable on hover + +/** + * A coarse type for the `data` prop returned by `useQuery`. + * + * ```js + * { + * data: { + * post: { ... } + * } + * } + * ``` + */ +export type DataObject = { [key: string]: unknown } +/** + * The main interface. + */ + +export interface CreateCellProps { + /** + * The GraphQL syntax tree to execute or function to call that returns it. + * If `QUERY` is a function, it's called with the result of `beforeQuery`. + */ + QUERY: DocumentNode | ((variables: Record) => DocumentNode) + /** + * Parse `props` into query variables. Most of the time `props` are appropriate variables as is. + */ + beforeQuery?: + | ((props: CellProps) => { variables: CellVariables }) + | (() => { variables: CellVariables }) + /** + * Sanitize the data returned from the query. + */ + afterQuery?: (data: DataObject) => DataObject + /** + * How to decide if the result of a query should render the `Empty` component. + * The default implementation checks that the first field isn't `null` or an empty array. + * + * @example + * + * In the example below, only `users` is checked: + * + * ```js + * export const QUERY = gql` + * users { + * name + * } + * posts { + * title + * } + * ` + * ``` + */ + isEmpty?: ( + response: DataObject, + options: { + isDataEmpty: (data: DataObject) => boolean + } + ) => boolean + /** + * If the query's in flight and there's no stale data, render this. + */ + Loading?: React.FC> + /** + * If something went wrong, render this. + */ + Failure?: React.FC> + /** + * If no data was returned, render this. + */ + Empty?: React.FC> + /** + * If data was returned, render this. + */ + Success: React.FC> + /** + * What to call the Cell. Defaults to the filename. + */ + displayName?: string +} diff --git a/packages/web/src/components/createCell.test.tsx b/packages/web/src/components/cell/createCell.test.tsx similarity index 100% rename from packages/web/src/components/createCell.test.tsx rename to packages/web/src/components/cell/createCell.test.tsx diff --git a/packages/web/src/components/createCell.tsx b/packages/web/src/components/cell/createCell.tsx similarity index 57% rename from packages/web/src/components/createCell.tsx rename to packages/web/src/components/cell/createCell.tsx index 88bc7e5123fc..e6f0f6755ccf 100644 --- a/packages/web/src/components/createCell.tsx +++ b/packages/web/src/components/cell/createCell.tsx @@ -1,193 +1,14 @@ -import { ComponentProps, JSXElementConstructor, Suspense } from 'react' +import { Suspense } from 'react' -import { OperationVariables } from '@apollo/client' -import type { DocumentNode } from 'graphql' -import type { A } from 'ts-toolbelt' - -import { getOperationName } from '../graphql' +import { getOperationName } from '../../graphql' +import { useQuery } from '../GraphQLHooksProvider' import { useCellCacheContext } from './CellCacheContext' /** * This is part of how we let users swap out their GraphQL client while staying compatible with Cells. */ import { CellErrorBoundary } from './CellErrorBoundary' -import { useQuery } from './GraphQLHooksProvider' - -/** - * - * If the Cell has a `beforeQuery` function, then the variables are not required, - * but instead the arguments of the `beforeQuery` function are required. - * - * If the Cell does not have a `beforeQuery` function, then the variables are required. - * - * Note that a query that doesn't take any variables is defined as {[x: string]: never} - * The ternary at the end makes sure we don't include it, otherwise it won't allow merging any - * other custom props from the Success component. - * - */ -type CellPropsVariables = Cell extends { - beforeQuery: (...args: any[]) => any -} - ? Parameters[0] extends unknown - ? Record - : Parameters[0] - : GQLVariables extends Record - ? unknown - : GQLVariables - -/** - * Cell component props which is the combination of query variables and Success props. - */ -export type CellProps< - CellSuccess extends keyof JSX.IntrinsicElements | JSXElementConstructor, - GQLResult, - CellType, - GQLVariables -> = A.Compute< - Omit< - ComponentProps, - | keyof CellPropsVariables - | keyof GQLResult - | 'updating' - | 'queryResult' - > & - CellPropsVariables -> - -export type CellLoadingProps = { - queryResult?: Partial< - Omit, 'loading' | 'error' | 'data'> - > -} - -export type CellFailureProps = { - queryResult?: Partial< - Omit, 'loading' | 'error' | 'data'> - > - error?: QueryOperationResult['error'] | Error // for tests and storybook - /** - * @see {@link https://www.apollographql.com/docs/apollo-server/data/errors/#error-codes} - */ - errorCode?: string - updating?: boolean -} - -// aka guarantee that all properties in T exist -// This is necessary for Cells, because if it doesn't exist it'll go to Empty or Failure -type Guaranteed = { - [K in keyof T]-?: NonNullable -} - -/** - * Use this type, if you are forwarding on the data from your Cell's Success component - * Because Cells automatically checks for "empty", or "errors" - if you receive the data type in your - * Success component, it means the data is guaranteed (and non-optional) - * - * @params TData = Type of data based on your graphql query. This can be imported from 'types/graphql' - * @example - * import type {FindPosts} from 'types/graphql' - * - * const { post } = CellSuccessData - * - * post.id // post is non optional, so no need to do post?.id - * - */ -export type CellSuccessData = Omit, '__typename'> - -/** - * @MARK not sure about this partial, but we need to do this for tests and storybook. - * - * `updating` is just `loading` renamed; since Cells default to stale-while-refetch, - * this prop lets users render something like a spinner to show that a request is in-flight. - */ -export type CellSuccessProps< - TData = any, - TVariables extends OperationVariables = any -> = { - queryResult?: Partial< - Omit, 'loading' | 'error' | 'data'> - > - updating?: boolean -} & A.Compute> // pre-computing makes the types more readable on hover - -/** - * A coarse type for the `data` prop returned by `useQuery`. - * - * ```js - * { - * data: { - * post: { ... } - * } - * } - * ``` - */ -export type DataObject = { [key: string]: unknown } - -/** - * The main interface. - */ -export interface CreateCellProps { - /** - * The GraphQL syntax tree to execute or function to call that returns it. - * If `QUERY` is a function, it's called with the result of `beforeQuery`. - */ - QUERY: DocumentNode | ((variables: Record) => DocumentNode) - /** - * Parse `props` into query variables. Most of the time `props` are appropriate variables as is. - */ - beforeQuery?: - | ((props: CellProps) => { variables: CellVariables }) - | (() => { variables: CellVariables }) - /** - * Sanitize the data returned from the query. - */ - afterQuery?: (data: DataObject) => DataObject - /** - * How to decide if the result of a query should render the `Empty` component. - * The default implementation checks that the first field isn't `null` or an empty array. - * - * @example - * - * In the example below, only `users` is checked: - * - * ```js - * export const QUERY = gql` - * users { - * name - * } - * posts { - * title - * } - * ` - * ``` - */ - isEmpty?: ( - response: DataObject, - options: { - isDataEmpty: (data: DataObject) => boolean - } - ) => boolean - /** - * If the query's in flight and there's no stale data, render this. - */ - Loading?: React.FC> - /** - * If something went wrong, render this. - */ - Failure?: React.FC> - /** - * If no data was returned, render this. - */ - Empty?: React.FC> - /** - * If data was returned, render this. - */ - Success: React.FC> - /** - * What to call the Cell. Defaults to the filename. - */ - displayName?: string -} +import { DataObject, CreateCellProps } from './cellTypes' /** * The default `isEmpty` implementation. Checks if any of the field is `null` or an empty array. diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 019e25db6fbb..30aeafefb305 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -14,16 +14,17 @@ export { useSubscription, } from './components/GraphQLHooksProvider' -export * from './components/CellCacheContext' +export * from './components/cell/CellCacheContext' + +export { createCell } from './components/cell/createCell' export { - createCell, CellProps, CellFailureProps, CellLoadingProps, CellSuccessProps, CellSuccessData, -} from './components/createCell' +} from './components/cell/cellTypes' export * from './graphql' From 9109597451525be70a7ac9cf7cddc15a85a50a11 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 27 Aug 2023 15:22:45 +0700 Subject: [PATCH 02/20] Move empty helpers to file to share --- .../web/src/components/cell/createCell.tsx | 51 +---- .../components/cell/createSuspendingCell.tsx | 209 ++++++++++++++++++ .../web/src/components/cell/isCellEmpty.tsx | 48 ++++ 3 files changed, 259 insertions(+), 49 deletions(-) create mode 100644 packages/web/src/components/cell/isCellEmpty.tsx diff --git a/packages/web/src/components/cell/createCell.tsx b/packages/web/src/components/cell/createCell.tsx index e6f0f6755ccf..a370ac40d5fa 100644 --- a/packages/web/src/components/cell/createCell.tsx +++ b/packages/web/src/components/cell/createCell.tsx @@ -8,55 +8,8 @@ import { useCellCacheContext } from './CellCacheContext' * This is part of how we let users swap out their GraphQL client while staying compatible with Cells. */ import { CellErrorBoundary } from './CellErrorBoundary' -import { DataObject, CreateCellProps } from './cellTypes' - -/** - * The default `isEmpty` implementation. Checks if any of the field is `null` or an empty array. - * - * Consider the following queries. The former returns an object, the latter a list: - * - * ```js - * export const QUERY = gql` - * post { - * title - * } - * ` - * - * export const QUERY = gql` - * posts { - * title - * } - * ` - * ``` - * - * If either are "empty", they return: - * - * ```js - * { - * data: { - * post: null - * } - * } - * - * { - * data: { - * posts: [] - * } - * } - * ``` - * - * Note that the latter can return `null` as well depending on the SDL (`posts: [Post!]`). - * ``` - */ -function isFieldEmptyArray(field: unknown) { - return Array.isArray(field) && field.length === 0 -} - -function isDataEmpty(data: DataObject) { - return Object.values(data).every((fieldValue) => { - return fieldValue === null || isFieldEmptyArray(fieldValue) - }) -} +import { CreateCellProps } from './cellTypes' +import { isDataEmpty } from './isCellEmpty' /** * Creates a Cell out of a GraphQL query and components that track to its lifecycle. diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index e69de29bb2d1..a370ac40d5fa 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -0,0 +1,209 @@ +import { Suspense } from 'react' + +import { getOperationName } from '../../graphql' +import { useQuery } from '../GraphQLHooksProvider' + +import { useCellCacheContext } from './CellCacheContext' +/** + * This is part of how we let users swap out their GraphQL client while staying compatible with Cells. + */ +import { CellErrorBoundary } from './CellErrorBoundary' +import { CreateCellProps } from './cellTypes' +import { isDataEmpty } from './isCellEmpty' + +/** + * Creates a Cell out of a GraphQL query and components that track to its lifecycle. + */ +export function createCell< + CellProps extends Record, + CellVariables extends Record +>({ + QUERY, + beforeQuery = (props) => ({ + // By default, we assume that the props are the gql-variables. + variables: props as unknown as CellVariables, + /** + * We're duplicating these props here due to a suspected bug in Apollo Client v3.5.4 + * (it doesn't seem to be respecting `defaultOptions` in `RedwoodApolloProvider`.) + * + * @see {@link https://github.com/apollographql/apollo-client/issues/9105} + */ + fetchPolicy: 'cache-and-network', + notifyOnNetworkStatusChange: true, + }), + afterQuery = (data) => ({ ...data }), + isEmpty = isDataEmpty, + Loading = () => <>Loading..., + Failure, + Empty, + Success, + displayName = 'Cell', +}: CreateCellProps): React.FC { + function NamedCell(props: React.PropsWithChildren) { + /** + * Right now, Cells don't render `children`. + */ + const { children: _, ...variables } = props + const options = beforeQuery(variables as CellProps) + const query = typeof QUERY === 'function' ? QUERY(options) : QUERY + + // queryRest includes `variables: { ... }`, with any variables returned + // from beforeQuery + let { + // eslint-disable-next-line prefer-const + error, + loading, + data, + ...queryResult + } = useQuery(query, options) + + if (globalThis.__REDWOOD__PRERENDERING) { + // __REDWOOD__PRERENDERING will always either be set, or not set. So + // rules-of-hooks are still respected, even though we wrap this in an if + // statement + /* eslint-disable-next-line react-hooks/rules-of-hooks */ + const { queryCache } = useCellCacheContext() + const operationName = getOperationName(query) + + let cacheKey + + if (operationName) { + cacheKey = operationName + '_' + JSON.stringify(variables) + } else { + const cellName = displayName === 'Cell' ? 'the cell' : displayName + + throw new Error( + `The gql query in ${cellName} is missing an operation name. ` + + 'Something like FindBlogPostQuery in ' + + '`query FindBlogPostQuery($id: Int!)`' + ) + } + + const queryInfo = queryCache[cacheKey] + + // This is true when the graphql handler couldn't be loaded + // So we fallback to the loading state + if (queryInfo?.renderLoading) { + loading = true + } else { + if (queryInfo?.hasProcessed) { + loading = false + data = queryInfo.data + + // All of the gql client's props aren't available when pre-rendering, + // so using `any` here + queryResult = { variables } as any + } else { + queryCache[cacheKey] || + (queryCache[cacheKey] = { + query, + variables: options.variables, + hasProcessed: false, + }) + } + } + } + + if (error) { + if (Failure) { + // errorCode is not part of the type returned by useQuery + // but it is returned as part of the queryResult + type QueryResultWithErrorCode = typeof queryResult & { + errorCode: string + } + + return ( + + ) + } else { + throw error + } + } else if (data) { + const afterQueryData = afterQuery(data) + + if (isEmpty(data, { isDataEmpty }) && Empty) { + return ( + + ) + } else { + return ( + + ) + } + } else if (loading) { + return + } else { + /** + * There really shouldn't be an `else` here, but like any piece of software, GraphQL clients have bugs. + * If there's no `error` and there's no `data` and we're not `loading`, something's wrong. Most likely with the cache. + * + * @see {@link https://github.com/redwoodjs/redwood/issues/2473#issuecomment-971864604} + */ + console.warn( + `If you're using Apollo Client, check for its debug logs here in the console, which may help explain the error.` + ) + throw new Error( + 'Cannot render Cell: reached an unexpected state where the query succeeded but `data` is `null`. If this happened in Storybook, your query could be missing fields; otherwise this is most likely a GraphQL caching bug. Note that adding an `id` field to all the fields on your query may fix the issue.' + ) + } + } + + NamedCell.displayName = displayName + + return (props: CellProps) => { + if (RWJS_ENV.RWJS_EXP_STREAMING_SSR) { + /** @TODO (STREAMING): Want to review full Cell lifecycle with Dom and Kris + * + * There's complexity here that I'm 70% sure I'm not capturing + * See notes below about queryResult. How can we refactor createCell so that it's available? + * Keep in mind we may need the ability to switch between useQuery and useSuspenseQuery + * + */ + + const FailureComponent = (fprops: any) => { + if (!Failure) { + return ( + <> +

Cell rendering failure. No Error component supplied.

+
{fprops.error}
+ + ) + } + + // @TODO (STREAMING) query-result not available here, because it comes from inside NamedCell + // How do we pass refetch, etc. in? + return + } + + return ( + + }> + + + + ) + } + + return + } +} diff --git a/packages/web/src/components/cell/isCellEmpty.tsx b/packages/web/src/components/cell/isCellEmpty.tsx new file mode 100644 index 000000000000..6443beeda882 --- /dev/null +++ b/packages/web/src/components/cell/isCellEmpty.tsx @@ -0,0 +1,48 @@ +import { DataObject } from './cellTypes'; + +/** + * The default `isEmpty` implementation. Checks if any of the field is `null` or an empty array. + * + * Consider the following queries. The former returns an object, the latter a list: + * + * ```js + * export const QUERY = gql` + * post { + * title + * } + * ` + * + * export const QUERY = gql` + * posts { + * title + * } + * ` + * ``` + * + * If either are "empty", they return: + * + * ```js + * { + * data: { + * post: null + * } + * } + * + * { + * data: { + * posts: [] + * } + * } + * ``` + * + * Note that the latter can return `null` as well depending on the SDL (`posts: [Post!]`). + * ``` + */ +function isFieldEmptyArray(field: unknown) { + return Array.isArray(field) && field.length === 0; +} +export function isDataEmpty(data: DataObject) { + return Object.values(data).every((fieldValue) => { + return fieldValue === null || isFieldEmptyArray(fieldValue); + }); +} From fecacd3331552b9dbf810704d799a2c73339889b Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 27 Aug 2023 17:38:35 +0700 Subject: [PATCH 03/20] createSuspenseCell --- .../src/components/cell/CellErrorBoundary.tsx | 9 +- .../web/src/components/cell/cellTypes.tsx | 44 +++- .../web/src/components/cell/createCell.tsx | 42 +--- .../components/cell/createSuspendingCell.tsx | 235 ++++++------------ 4 files changed, 123 insertions(+), 207 deletions(-) diff --git a/packages/web/src/components/cell/CellErrorBoundary.tsx b/packages/web/src/components/cell/CellErrorBoundary.tsx index 20316cac1781..1abbb51fce2d 100644 --- a/packages/web/src/components/cell/CellErrorBoundary.tsx +++ b/packages/web/src/components/cell/CellErrorBoundary.tsx @@ -5,7 +5,7 @@ import type { CellFailureProps } from './cellTypes' type CellErrorBoundaryProps = { // Note that the fallback has to be an FC, not a Node // because the error comes from this component's state - fallback: React.FC + fallback?: React.FC children: React.ReactNode } @@ -33,15 +33,18 @@ export class CellErrorBoundary extends React.Component< } render() { + // The fallback is constructed with all the props required, except error and errorCode + // in createSusepndingCell.tsx const { fallback: Fallback } = this.props - if (this.state.hasError) { + + // @TODO what happens when no Fallback supplied?? + if (this.state.hasError && Fallback) { return ( ) } diff --git a/packages/web/src/components/cell/cellTypes.tsx b/packages/web/src/components/cell/cellTypes.tsx index 400302a38005..90a3b0881681 100644 --- a/packages/web/src/components/cell/cellTypes.tsx +++ b/packages/web/src/components/cell/cellTypes.tsx @@ -1,6 +1,12 @@ import { ComponentProps, JSXElementConstructor } from 'react' -import { OperationVariables } from '@apollo/client' +import type { + ApolloClient, + NetworkStatus, + OperationVariables, + QueryReference, + UseBackgroundQueryResult, +} from '@apollo/client' import type { DocumentNode } from 'graphql' import type { A } from 'ts-toolbelt' @@ -46,15 +52,11 @@ export type CellProps< > export type CellLoadingProps = { - queryResult?: Partial< - Omit, 'loading' | 'error' | 'data'> - > + queryResult?: NonSuspenseCellQueryResult | SuspenseCellQueryResult } export type CellFailureProps = { - queryResult?: Partial< - Omit, 'loading' | 'error' | 'data'> - > + queryResult?: NonSuspenseCellQueryResult | SuspenseCellQueryResult error?: QueryOperationResult['error'] | Error // for tests and storybook /** @@ -95,9 +97,7 @@ export type CellSuccessProps< TData = any, TVariables extends OperationVariables = any > = { - queryResult?: Partial< - Omit, 'loading' | 'error' | 'data'> - > + queryResult?: NonSuspenseCellQueryResult | SuspenseCellQueryResult updating?: boolean } & A.Compute> // pre-computing makes the types more readable on hover @@ -179,3 +179,27 @@ export interface CreateCellProps { */ displayName?: string } + +export type SuperSuccessProps = React.PropsWithChildren< + Record +> & { + RWJS_cellQueryRef: QueryReference // from useBackgroundQuery + suspenseQueryResult: SuspenseCellQueryResult + userProps: Record // we don't really care about the types here, we are just forwarding on +} + +export type NonSuspenseCellQueryResult< + TVariables extends OperationVariables = any +> = Partial< + Omit, 'loading' | 'error' | 'data'> +> + +// We call this queryResult in createCell, sadly a very overloaded term +// This is just the extra things returned from useXQuery hooks +export interface SuspenseCellQueryResult extends UseBackgroundQueryResult { + client: ApolloClient + // previousData?: any, + // observable: ObservableQuery, + networkStatus: NetworkStatus + called: boolean // not available in useBackgroundQuery I think +} diff --git a/packages/web/src/components/cell/createCell.tsx b/packages/web/src/components/cell/createCell.tsx index a370ac40d5fa..7464e28fb0dd 100644 --- a/packages/web/src/components/cell/createCell.tsx +++ b/packages/web/src/components/cell/createCell.tsx @@ -1,13 +1,10 @@ -import { Suspense } from 'react' - import { getOperationName } from '../../graphql' -import { useQuery } from '../GraphQLHooksProvider' - -import { useCellCacheContext } from './CellCacheContext' /** * This is part of how we let users swap out their GraphQL client while staying compatible with Cells. */ -import { CellErrorBoundary } from './CellErrorBoundary' +import { useQuery } from '../GraphQLHooksProvider' + +import { useCellCacheContext } from './CellCacheContext' import { CreateCellProps } from './cellTypes' import { isDataEmpty } from './isCellEmpty' @@ -171,39 +168,6 @@ export function createCell< NamedCell.displayName = displayName return (props: CellProps) => { - if (RWJS_ENV.RWJS_EXP_STREAMING_SSR) { - /** @TODO (STREAMING): Want to review full Cell lifecycle with Dom and Kris - * - * There's complexity here that I'm 70% sure I'm not capturing - * See notes below about queryResult. How can we refactor createCell so that it's available? - * Keep in mind we may need the ability to switch between useQuery and useSuspenseQuery - * - */ - - const FailureComponent = (fprops: any) => { - if (!Failure) { - return ( - <> -

Cell rendering failure. No Error component supplied.

-
{fprops.error}
- - ) - } - - // @TODO (STREAMING) query-result not available here, because it comes from inside NamedCell - // How do we pass refetch, etc. in? - return - } - - return ( - - }> - - - - ) - } - return } } diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index a370ac40d5fa..2547c57d4d38 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -1,20 +1,30 @@ import { Suspense } from 'react' -import { getOperationName } from '../../graphql' -import { useQuery } from '../GraphQLHooksProvider' +// @TODO(STREAMING): We are directly importing from Apollo here +// because useBgQuery, and useReadQuery are Apollo 3.8+ specific +import { NetworkStatus, QueryReference, useApolloClient } from '@apollo/client' +import { + useBackgroundQuery, + useReadQuery, +} from '@apollo/experimental-nextjs-app-support/ssr' -import { useCellCacheContext } from './CellCacheContext' /** * This is part of how we let users swap out their GraphQL client while staying compatible with Cells. */ import { CellErrorBoundary } from './CellErrorBoundary' -import { CreateCellProps } from './cellTypes' +import { + SuspenseCellQueryResult, + CreateCellProps, + DataObject, + SuperSuccessProps, +} from './cellTypes' import { isDataEmpty } from './isCellEmpty' /** - * Creates a Cell out of a GraphQL query and components that track to its lifecycle. + * Creates a Cell ~~ with Apollo Client only ~~ + * using the hooks useBackgroundQuery and useReadQuery */ -export function createCell< +export function createSuspendingCell< CellProps extends Record, CellVariables extends Record >({ @@ -39,171 +49,86 @@ export function createCell< Success, displayName = 'Cell', }: CreateCellProps): React.FC { - function NamedCell(props: React.PropsWithChildren) { - /** - * Right now, Cells don't render `children`. - */ - const { children: _, ...variables } = props - const options = beforeQuery(variables as CellProps) - const query = typeof QUERY === 'function' ? QUERY(options) : QUERY + function SuperSuccess(props: SuperSuccessProps) { + const { RWJS_cellQueryRef, suspenseQueryResult, userProps } = props - // queryRest includes `variables: { ... }`, with any variables returned - // from beforeQuery - let { - // eslint-disable-next-line prefer-const - error, - loading, - data, - ...queryResult - } = useQuery(query, options) + const { data, networkStatus } = useReadQuery(RWJS_cellQueryRef) + const afterQueryData = afterQuery(data as DataObject) - if (globalThis.__REDWOOD__PRERENDERING) { - // __REDWOOD__PRERENDERING will always either be set, or not set. So - // rules-of-hooks are still respected, even though we wrap this in an if - // statement - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - const { queryCache } = useCellCacheContext() - const operationName = getOperationName(query) + const queryResultWithNetworkStatus = { + ...suspenseQueryResult, + networkStatus, + } - let cacheKey + if (isEmpty(data, { isDataEmpty }) && Empty) { + return ( + // @ts-expect-error HELP, cant make queryResult type work + + ) + } - if (operationName) { - cacheKey = operationName + '_' + JSON.stringify(variables) - } else { - const cellName = displayName === 'Cell' ? 'the cell' : displayName + return ( + // @ts-expect-error HELP, cant make queryResult type work + + ) + } - throw new Error( - `The gql query in ${cellName} is missing an operation name. ` + - 'Something like FindBlogPostQuery in ' + - '`query FindBlogPostQuery($id: Int!)`' - ) - } + SuperSuccess.displayName = displayName - const queryInfo = queryCache[cacheKey] + // @NOTE: Note that we are returning a HoC here! + return (props: CellProps) => { + /** + * Right now, Cells don't render `children`. + */ + const { children: _, ...variables } = props + const options = beforeQuery(variables as CellProps) + const query = typeof QUERY === 'function' ? QUERY(options) : QUERY + const [queryRef, other] = useBackgroundQuery(query, options) - // This is true when the graphql handler couldn't be loaded - // So we fallback to the loading state - if (queryInfo?.renderLoading) { - loading = true - } else { - if (queryInfo?.hasProcessed) { - loading = false - data = queryInfo.data + const client = useApolloClient() - // All of the gql client's props aren't available when pre-rendering, - // so using `any` here - queryResult = { variables } as any - } else { - queryCache[cacheKey] || - (queryCache[cacheKey] = { - query, - variables: options.variables, - hasProcessed: false, - }) - } - } + const suspenseQueryResult: SuspenseCellQueryResult = { + client, + ...other, + called: !!queryRef, + // @MARK set this to loading here, gets over-ridden in SuperSuccess + networkStatus: NetworkStatus.loading, } - if (error) { - if (Failure) { - // errorCode is not part of the type returned by useQuery - // but it is returned as part of the queryResult - type QueryResultWithErrorCode = typeof queryResult & { - errorCode: string - } + // @TODO(STREAMING) removed prerender handling here + // Until we decide how/if we do prerendering + const FailureComponent = (fprops: any) => { + if (!Failure) { return ( - + <> +

Cell rendering failure. No Error component supplied.

+
{fprops.error}
+ ) - } else { - throw error } - } else if (data) { - const afterQueryData = afterQuery(data) - if (isEmpty(data, { isDataEmpty }) && Empty) { - return ( - - ) - } else { - return ( - - ) - } - } else if (loading) { - return - } else { - /** - * There really shouldn't be an `else` here, but like any piece of software, GraphQL clients have bugs. - * If there's no `error` and there's no `data` and we're not `loading`, something's wrong. Most likely with the cache. - * - * @see {@link https://github.com/redwoodjs/redwood/issues/2473#issuecomment-971864604} - */ - console.warn( - `If you're using Apollo Client, check for its debug logs here in the console, which may help explain the error.` - ) - throw new Error( - 'Cannot render Cell: reached an unexpected state where the query succeeded but `data` is `null`. If this happened in Storybook, your query could be missing fields; otherwise this is most likely a GraphQL caching bug. Note that adding an `id` field to all the fields on your query may fix the issue.' - ) + return } - } - - NamedCell.displayName = displayName - return (props: CellProps) => { - if (RWJS_ENV.RWJS_EXP_STREAMING_SSR) { - /** @TODO (STREAMING): Want to review full Cell lifecycle with Dom and Kris - * - * There's complexity here that I'm 70% sure I'm not capturing - * See notes below about queryResult. How can we refactor createCell so that it's available? - * Keep in mind we may need the ability to switch between useQuery and useSuspenseQuery - * - */ - - const FailureComponent = (fprops: any) => { - if (!Failure) { - return ( - <> -

Cell rendering failure. No Error component supplied.

-
{fprops.error}
- - ) - } - - // @TODO (STREAMING) query-result not available here, because it comes from inside NamedCell - // How do we pass refetch, etc. in? - return - } - - return ( - - }> - - - - ) - } - - return + return ( + + }> + } + suspenseQueryResult={suspenseQueryResult} + /> + + + ) } } From 5468b009db597045b475703cb1eaaca30c996527 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 27 Aug 2023 17:38:46 +0700 Subject: [PATCH 04/20] Add alias to import apollo provider directly --- packages/web/apollo/suspense.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 packages/web/apollo/suspense.js diff --git a/packages/web/apollo/suspense.js b/packages/web/apollo/suspense.js new file mode 100644 index 000000000000..455fad70893f --- /dev/null +++ b/packages/web/apollo/suspense.js @@ -0,0 +1,2 @@ +/* eslint-env es6, commonjs */ +module.exports = require('../dist/apollo/suspense') From 474c234af87843c754c34d2a7740ffcdef86ce4e Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 27 Aug 2023 17:41:22 +0700 Subject: [PATCH 05/20] Rename createSuspendingCell to createCell to make it easier on the babel plugin --- packages/web/src/components/cell/createSuspendingCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index 2547c57d4d38..7bf5e330f4a1 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -24,7 +24,7 @@ import { isDataEmpty } from './isCellEmpty' * Creates a Cell ~~ with Apollo Client only ~~ * using the hooks useBackgroundQuery and useReadQuery */ -export function createSuspendingCell< +export function createCell< CellProps extends Record, CellVariables extends Record >({ From e91475d52d96a15dc587e5fe79755a95a6dd420d Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 27 Aug 2023 18:52:46 +0700 Subject: [PATCH 06/20] Get it working by passing in all Apollo hooks --- packages/web/src/apollo/index.tsx | 6 ++ packages/web/src/apollo/suspense.tsx | 14 ++- packages/web/src/apollo/typeOverride.ts | 12 +++ .../src/components/GraphQLHooksProvider.tsx | 92 ++++++++++++++++++- .../web/src/components/cell/createCell.tsx | 8 +- .../components/cell/createSuspendingCell.tsx | 13 ++- 6 files changed, 130 insertions(+), 15 deletions(-) diff --git a/packages/web/src/apollo/index.tsx b/packages/web/src/apollo/index.tsx index d7060fdd6876..cb845d9e4036 100644 --- a/packages/web/src/apollo/index.tsx +++ b/packages/web/src/apollo/index.tsx @@ -19,6 +19,9 @@ const { useQuery, useMutation, useSubscription, + useBackgroundQuery, + useReadQuery, + useSuspenseQuery, setLogVerbosity: apolloSetLogVerbosity, } = apolloClient @@ -314,6 +317,9 @@ export const RedwoodApolloProvider: React.FunctionComponent<{ useQuery={useQuery} useMutation={useMutation} useSubscription={useSubscription} + useBackgroundQuery={useBackgroundQuery} + useReadQuery={useReadQuery} + useSuspenseQuery={useSuspenseQuery} > {children} diff --git a/packages/web/src/apollo/suspense.tsx b/packages/web/src/apollo/suspense.tsx index fdbd508b619a..addf21fdbc39 100644 --- a/packages/web/src/apollo/suspense.tsx +++ b/packages/web/src/apollo/suspense.tsx @@ -3,7 +3,8 @@ * This is a lift and shift of the original ApolloProvider * but with suspense specific bits. Look for @MARK to find bits I've changed * - * Done this way, to avoid making changes breaking on main. + * Done this way, to avoid making changes breaking on main, due to the experimental-nextjs import + * Eventually we will have one ApolloProvider, not multiple. */ import type { @@ -24,6 +25,9 @@ import { NextSSRApolloClient, NextSSRInMemoryCache, useSuspenseQuery, + useBackgroundQuery, + useReadQuery, + useQuery, } from '@apollo/experimental-nextjs-app-support/ssr' import { UseAuth, useNoAuth } from '@redwoodjs/auth' @@ -229,12 +233,12 @@ export const RedwoodApolloProvider: React.FunctionComponent<{ logLevel={logLevel} > {children} diff --git a/packages/web/src/apollo/typeOverride.ts b/packages/web/src/apollo/typeOverride.ts index 139349e163eb..d4d7e6dd6909 100644 --- a/packages/web/src/apollo/typeOverride.ts +++ b/packages/web/src/apollo/typeOverride.ts @@ -7,6 +7,8 @@ import type { OperationVariables, SubscriptionHookOptions, SubscriptionResult, + UseSuspenseQueryResult, + SuspenseQueryHookOptions, } from '@apollo/client' // @MARK: Override relevant types from Apollo here @@ -36,6 +38,16 @@ declare global { TData, TVariables extends OperationVariables > extends SubscriptionHookOptions {} + + interface SuspenseQueryOperationResult< + TData = any, + TVariables extends OperationVariables = OperationVariables + > extends UseSuspenseQueryResult {} + + interface GraphQLSuspenseQueryHookOptions< + TData, + TVariables extends OperationVariables + > extends SuspenseQueryHookOptions {} } export {} diff --git a/packages/web/src/components/GraphQLHooksProvider.tsx b/packages/web/src/components/GraphQLHooksProvider.tsx index db9e985cb586..a501b2d5e9ad 100644 --- a/packages/web/src/components/GraphQLHooksProvider.tsx +++ b/packages/web/src/components/GraphQLHooksProvider.tsx @@ -1,6 +1,19 @@ -import { OperationVariables } from '@apollo/client' +import type { + OperationVariables, + SuspenseQueryHookOptions, + useBackgroundQuery as apolloUseBackgroundQuery, + useReadQuery as apolloUseReadQuery, +} from '@apollo/client' import type { DocumentNode } from 'graphql' +/** + * @NOTE + * The types QueryOperationResult, MutationOperationResult, SubscriptionOperationResult, and SuspenseQueryOperationResult + * are overridden in packages/web/src/apollo/typeOverride.ts. This was originally so that you could bring your own gql client. + * + * The default (empty) types are defined in packages/web/src/global.web-auto-imports.ts + */ + type DefaultUseQueryType = < TData = any, TVariables extends OperationVariables = GraphQLOperationVariables @@ -24,14 +37,29 @@ type DefaultUseSubscriptionType = < subscription: DocumentNode, options?: GraphQLSubscriptionHookOptions ) => SubscriptionOperationResult + +type DefaultUseSuspenseType = < + TData = any, + TVariables extends OperationVariables = GraphQLOperationVariables +>( + query: DocumentNode, + options?: GraphQLSuspenseQueryHookOptions +) => SuspenseQueryOperationResult + export interface GraphQLHooks< TuseQuery = DefaultUseQueryType, TuseMutation = DefaultUseMutationType, - TuseSubscription = DefaultUseSubscriptionType + TuseSubscription = DefaultUseSubscriptionType, + TuseSuspenseQuery = DefaultUseSuspenseType > { useQuery: TuseQuery useMutation: TuseMutation useSubscription: TuseSubscription + useSuspenseQuery: TuseSuspenseQuery + // @NOTE note that we aren't using typeoverride here. + // This is because useBackgroundQuery and useReadQuery are apollo specific hooks. + useBackgroundQuery: typeof apolloUseBackgroundQuery + useReadQuery: typeof apolloUseReadQuery } export const GraphQLHooksContext = React.createContext({ @@ -50,13 +78,37 @@ export const GraphQLHooksContext = React.createContext({ 'You must register a useSubscription hook via the `GraphQLHooksProvider`' ) }, + useSuspenseQuery: () => { + throw new Error( + 'You must register a useSuspenseQuery hook via the `GraphQLHooksProvider`.' + ) + }, + + // These are apollo specific hooks! + useBackgroundQuery: () => { + throw new Error( + 'You must register a useBackgroundQuery hook via the `GraphQLHooksProvider`. Make sure you are importing the correct Apollo provider in App.tsx' + ) + }, + + useReadQuery: () => { + throw new Error( + 'You must register a useReadQuery hook via the `GraphQLHooksProvider`. Make sure you are importing the correct Apollo provider in App.tsx' + ) + }, }) interface GraphQlHooksProviderProps< TuseQuery = DefaultUseQueryType, TuseMutation = DefaultUseMutationType, - TuseSubscription = DefaultUseSubscriptionType -> extends GraphQLHooks { + TuseSubscription = DefaultUseSubscriptionType, + TuseSuspenseQuery = DefaultUseSuspenseType +> extends GraphQLHooks< + TuseQuery, + TuseMutation, + TuseSubscription, + TuseSuspenseQuery + > { children: React.ReactNode } @@ -74,6 +126,9 @@ export const GraphQLHooksProvider = < useQuery, useMutation, useSubscription, + useSuspenseQuery, + useBackgroundQuery, + useReadQuery, children, }: GraphQlHooksProviderProps) => { return ( @@ -82,6 +137,9 @@ export const GraphQLHooksProvider = < useQuery, useMutation, useSubscription, + useSuspenseQuery, + useBackgroundQuery, + useReadQuery, }} > {children} @@ -127,3 +185,29 @@ export function useSubscription< TVariables >(query, options) } + +export function useSuspenseQuery< + TData = any, + TVariables extends OperationVariables = GraphQLOperationVariables +>( + query: DocumentNode, + options?: SuspenseQueryHookOptions +): SuspenseQueryOperationResult { + return React.useContext(GraphQLHooksContext).useSuspenseQuery< + TData, + TVariables + >(query, options) +} + +export const useBackgroundQuery: typeof apolloUseBackgroundQuery = ( + ...args +) => { + // @TODO something about the apollo types here mean I need to override the return type + return React.useContext(GraphQLHooksContext).useBackgroundQuery( + ...args + ) as any +} + +export const useReadQuery: typeof apolloUseReadQuery = (...args) => { + return React.useContext(GraphQLHooksContext).useReadQuery(...args) +} diff --git a/packages/web/src/components/cell/createCell.tsx b/packages/web/src/components/cell/createCell.tsx index 7464e28fb0dd..74f5479559b1 100644 --- a/packages/web/src/components/cell/createCell.tsx +++ b/packages/web/src/components/cell/createCell.tsx @@ -6,12 +6,18 @@ import { useQuery } from '../GraphQLHooksProvider' import { useCellCacheContext } from './CellCacheContext' import { CreateCellProps } from './cellTypes' +import { createSuspendingCell } from './createSuspendingCell' import { isDataEmpty } from './isCellEmpty' +// 👇 Note how we switch which cell factory to use! +export const createCell = RWJS_ENV.RWJS_EXP_STREAMING_SSR + ? createSuspendingCell + : createNonSuspendingCell + /** * Creates a Cell out of a GraphQL query and components that track to its lifecycle. */ -export function createCell< +function createNonSuspendingCell< CellProps extends Record, CellVariables extends Record >({ diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index 7bf5e330f4a1..d347281631ac 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -3,10 +3,8 @@ import { Suspense } from 'react' // @TODO(STREAMING): We are directly importing from Apollo here // because useBgQuery, and useReadQuery are Apollo 3.8+ specific import { NetworkStatus, QueryReference, useApolloClient } from '@apollo/client' -import { - useBackgroundQuery, - useReadQuery, -} from '@apollo/experimental-nextjs-app-support/ssr' + +import { useBackgroundQuery, useReadQuery } from '../GraphQLHooksProvider' /** * This is part of how we let users swap out their GraphQL client while staying compatible with Cells. @@ -24,7 +22,7 @@ import { isDataEmpty } from './isCellEmpty' * Creates a Cell ~~ with Apollo Client only ~~ * using the hooks useBackgroundQuery and useReadQuery */ -export function createCell< +export function createSuspendingCell< CellProps extends Record, CellVariables extends Record >({ @@ -52,6 +50,11 @@ export function createCell< function SuperSuccess(props: SuperSuccessProps) { const { RWJS_cellQueryRef, suspenseQueryResult, userProps } = props + console.log('xxxxx definitely in super successssss!') + console.log('xxxxx definitely in super successssss!') + console.log('xxxxx definitely in super successssss!') + console.log('xxxxx definitely in super successssss!') + const { data, networkStatus } = useReadQuery(RWJS_cellQueryRef) const afterQueryData = afterQuery(data as DataObject) From 94fa45c9d69938f29541eedb4a14227e72af9c22 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 27 Aug 2023 18:53:37 +0700 Subject: [PATCH 07/20] Comments --- packages/web/src/components/cell/createSuspendingCell.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index d347281631ac..994b1668898f 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -49,12 +49,6 @@ export function createSuspendingCell< }: CreateCellProps): React.FC { function SuperSuccess(props: SuperSuccessProps) { const { RWJS_cellQueryRef, suspenseQueryResult, userProps } = props - - console.log('xxxxx definitely in super successssss!') - console.log('xxxxx definitely in super successssss!') - console.log('xxxxx definitely in super successssss!') - console.log('xxxxx definitely in super successssss!') - const { data, networkStatus } = useReadQuery(RWJS_cellQueryRef) const afterQueryData = afterQuery(data as DataObject) @@ -65,6 +59,7 @@ export function createSuspendingCell< if (isEmpty(data, { isDataEmpty }) && Empty) { return ( + // @TODO HELP! // @ts-expect-error HELP, cant make queryResult type work Date: Mon, 28 Aug 2023 12:38:22 +0700 Subject: [PATCH 08/20] Cleanup & "Fix" types --- .../web/src/components/cell/cellTypes.tsx | 12 ++- .../components/cell/createSuspendingCell.tsx | 75 +++++++++---------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/packages/web/src/components/cell/cellTypes.tsx b/packages/web/src/components/cell/cellTypes.tsx index 90a3b0881681..48626917dd60 100644 --- a/packages/web/src/components/cell/cellTypes.tsx +++ b/packages/web/src/components/cell/cellTypes.tsx @@ -183,7 +183,7 @@ export interface CreateCellProps { export type SuperSuccessProps = React.PropsWithChildren< Record > & { - RWJS_cellQueryRef: QueryReference // from useBackgroundQuery + queryRef: QueryReference // from useBackgroundQuery suspenseQueryResult: SuspenseCellQueryResult userProps: Record // we don't really care about the types here, we are just forwarding on } @@ -198,8 +198,14 @@ export type NonSuspenseCellQueryResult< // This is just the extra things returned from useXQuery hooks export interface SuspenseCellQueryResult extends UseBackgroundQueryResult { client: ApolloClient - // previousData?: any, - // observable: ObservableQuery, + // fetchMore & refetch come from UseBackgroundQueryResult networkStatus: NetworkStatus + // Stuff not here: called: boolean // not available in useBackgroundQuery I think + // previousData?: any, + // observable: ObservableQuery, + // startPolling + // stopPolling + // subscribeToMore + // updateQuery } diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index 994b1668898f..f628eed308c2 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -11,56 +11,59 @@ import { useBackgroundQuery, useReadQuery } from '../GraphQLHooksProvider' */ import { CellErrorBoundary } from './CellErrorBoundary' import { - SuspenseCellQueryResult, CreateCellProps, DataObject, SuperSuccessProps, + SuspenseCellQueryResult, } from './cellTypes' import { isDataEmpty } from './isCellEmpty' +type AnyObj = Record /** * Creates a Cell ~~ with Apollo Client only ~~ * using the hooks useBackgroundQuery and useReadQuery + * */ export function createSuspendingCell< - CellProps extends Record, - CellVariables extends Record ->({ - QUERY, - beforeQuery = (props) => ({ - // By default, we assume that the props are the gql-variables. - variables: props as unknown as CellVariables, - /** - * We're duplicating these props here due to a suspected bug in Apollo Client v3.5.4 - * (it doesn't seem to be respecting `defaultOptions` in `RedwoodApolloProvider`.) - * - * @see {@link https://github.com/apollographql/apollo-client/issues/9105} - */ - fetchPolicy: 'cache-and-network', - notifyOnNetworkStatusChange: true, - }), - afterQuery = (data) => ({ ...data }), - isEmpty = isDataEmpty, - Loading = () => <>Loading..., - Failure, - Empty, - Success, - displayName = 'Cell', -}: CreateCellProps): React.FC { + CellProps extends AnyObj, + CellVariables extends AnyObj +>( + createCellProps: CreateCellProps // 👈 AnyObj, because using CellProps causes a TS error +): React.FC { + const { + QUERY, + beforeQuery = (props) => ({ + // By default, we assume that the props are the gql-variables. + variables: props as unknown as CellVariables, + /** + * We're duplicating these props here due to a suspected bug in Apollo Client v3.5.4 + * (it doesn't seem to be respecting `defaultOptions` in `RedwoodApolloProvider`.) + * + * @see {@link https://github.com/apollographql/apollo-client/issues/9105} + */ + fetchPolicy: 'cache-and-network', + notifyOnNetworkStatusChange: true, + }), + afterQuery = (data) => ({ ...data }), + isEmpty = isDataEmpty, + Loading = () => <>Loading..., + Failure, + Empty, + Success, + displayName = 'Cell', + } = createCellProps function SuperSuccess(props: SuperSuccessProps) { - const { RWJS_cellQueryRef, suspenseQueryResult, userProps } = props - const { data, networkStatus } = useReadQuery(RWJS_cellQueryRef) + const { queryRef, suspenseQueryResult, userProps } = props + const { data, networkStatus } = useReadQuery(queryRef) const afterQueryData = afterQuery(data as DataObject) - const queryResultWithNetworkStatus = { + const queryResultWithNetworkStatus: SuspenseCellQueryResult = { ...suspenseQueryResult, networkStatus, } if (isEmpty(data, { isDataEmpty }) && Empty) { return ( - // @TODO HELP! - // @ts-expect-error HELP, cant make queryResult type work { if (!Failure) { - return ( - <> -

Cell rendering failure. No Error component supplied.

-
{fprops.error}
- - ) + throw fprops.error } return @@ -123,7 +120,7 @@ export function createSuspendingCell< }> } + queryRef={queryRef as QueryReference} suspenseQueryResult={suspenseQueryResult} /> From 90dd8df4da1e69e26d70c054e5caf30f8e0eea50 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 28 Aug 2023 13:34:25 +0700 Subject: [PATCH 09/20] Add some more queryResult stuff --- .../web/src/components/cell/cellTypes.tsx | 14 ++++++++---- .../components/cell/createSuspendingCell.tsx | 22 +++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/web/src/components/cell/cellTypes.tsx b/packages/web/src/components/cell/cellTypes.tsx index 48626917dd60..5d08aba4f1ce 100644 --- a/packages/web/src/components/cell/cellTypes.tsx +++ b/packages/web/src/components/cell/cellTypes.tsx @@ -3,6 +3,7 @@ import { ComponentProps, JSXElementConstructor } from 'react' import type { ApolloClient, NetworkStatus, + ObservableQuery, OperationVariables, QueryReference, UseBackgroundQueryResult, @@ -97,7 +98,9 @@ export type CellSuccessProps< TData = any, TVariables extends OperationVariables = any > = { - queryResult?: NonSuspenseCellQueryResult | SuspenseCellQueryResult + queryResult?: + | NonSuspenseCellQueryResult + | SuspenseCellQueryResult updating?: boolean } & A.Compute> // pre-computing makes the types more readable on hover @@ -184,7 +187,7 @@ export type SuperSuccessProps = React.PropsWithChildren< Record > & { queryRef: QueryReference // from useBackgroundQuery - suspenseQueryResult: SuspenseCellQueryResult + suspenseQueryResult: SuspenseCellQueryResult userProps: Record // we don't really care about the types here, we are just forwarding on } @@ -196,14 +199,17 @@ export type NonSuspenseCellQueryResult< // We call this queryResult in createCell, sadly a very overloaded term // This is just the extra things returned from useXQuery hooks -export interface SuspenseCellQueryResult extends UseBackgroundQueryResult { +export interface SuspenseCellQueryResult< + TData = any, + TVariables extends OperationVariables = any +> extends UseBackgroundQueryResult { client: ApolloClient // fetchMore & refetch come from UseBackgroundQueryResult + observable: ObservableQuery networkStatus: NetworkStatus // Stuff not here: called: boolean // not available in useBackgroundQuery I think // previousData?: any, - // observable: ObservableQuery, // startPolling // stopPolling // subscribeToMore diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index f628eed308c2..ca680d9e81f1 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -1,8 +1,7 @@ import { Suspense } from 'react' -// @TODO(STREAMING): We are directly importing from Apollo here -// because useBgQuery, and useReadQuery are Apollo 3.8+ specific -import { NetworkStatus, QueryReference, useApolloClient } from '@apollo/client' +import { QueryReference, useApolloClient } from '@apollo/client' +import type { InternalQueryReference } from '@apollo/client/react/cache/QueryReference' import { useBackgroundQuery, useReadQuery } from '../GraphQLHooksProvider' @@ -93,15 +92,22 @@ export function createSuspendingCell< const query = typeof QUERY === 'function' ? QUERY(options) : QUERY const [queryRef, other] = useBackgroundQuery(query, options) + // @TODO Waiting for feedback from Apollo team + const symbolToAccessQueryRef = Object.getOwnPropertySymbols(queryRef)[0] + // @ts-expect-error I know what Im doing + const __dangerouslyAccessInternalReference = queryRef[ + symbolToAccessQueryRef + ] as InternalQueryReference + const client = useApolloClient() const suspenseQueryResult: SuspenseCellQueryResult = { client, ...other, called: !!queryRef, - // @TODO This is not correct - // @MARK set this to loading here, gets over-ridden in SuperSuccess - networkStatus: NetworkStatus.loading, + networkStatus: + __dangerouslyAccessInternalReference?.result?.networkStatus, + observable: __dangerouslyAccessInternalReference?.observable, } // @TODO(STREAMING) removed prerender handling here @@ -117,7 +123,9 @@ export function createSuspendingCell< return ( - }> + } + > } From ad4b257b740e710aa6455dc1ded7a623f21b4d01 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 28 Aug 2023 13:55:11 +0700 Subject: [PATCH 10/20] Restore queryResult type. Problem wasn't on my end --- packages/web/src/components/cell/cellTypes.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/cell/cellTypes.tsx b/packages/web/src/components/cell/cellTypes.tsx index 5d08aba4f1ce..96a7678a60c6 100644 --- a/packages/web/src/components/cell/cellTypes.tsx +++ b/packages/web/src/components/cell/cellTypes.tsx @@ -98,9 +98,7 @@ export type CellSuccessProps< TData = any, TVariables extends OperationVariables = any > = { - queryResult?: - | NonSuspenseCellQueryResult - | SuspenseCellQueryResult + queryResult?: NonSuspenseCellQueryResult | SuspenseCellQueryResult updating?: boolean } & A.Compute> // pre-computing makes the types more readable on hover @@ -208,7 +206,7 @@ export interface SuspenseCellQueryResult< observable: ObservableQuery networkStatus: NetworkStatus // Stuff not here: - called: boolean // not available in useBackgroundQuery I think + called: boolean // can we assume if we have a queryRef its called? // previousData?: any, // startPolling // stopPolling From 042f488e86960942691b929ad9eff6d3ee1108a8 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 28 Aug 2023 15:09:42 +0700 Subject: [PATCH 11/20] Remove suspense proxy --- packages/web/apollo/suspense.js | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 packages/web/apollo/suspense.js diff --git a/packages/web/apollo/suspense.js b/packages/web/apollo/suspense.js deleted file mode 100644 index 455fad70893f..000000000000 --- a/packages/web/apollo/suspense.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-env es6, commonjs */ -module.exports = require('../dist/apollo/suspense') From 9daaae9ae0c2792213e06e4109b925e0231a9bde Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 28 Aug 2023 15:20:50 +0700 Subject: [PATCH 12/20] Update error boundary --- .../web/src/components/cell/CellErrorBoundary.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/cell/CellErrorBoundary.tsx b/packages/web/src/components/cell/CellErrorBoundary.tsx index 1abbb51fce2d..1b7b0c9734df 100644 --- a/packages/web/src/components/cell/CellErrorBoundary.tsx +++ b/packages/web/src/components/cell/CellErrorBoundary.tsx @@ -5,7 +5,7 @@ import type { CellFailureProps } from './cellTypes' type CellErrorBoundaryProps = { // Note that the fallback has to be an FC, not a Node // because the error comes from this component's state - fallback?: React.FC + fallback: React.FC children: React.ReactNode } @@ -29,7 +29,10 @@ export class CellErrorBoundary extends React.Component< componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { // @TODO do something with this? - console.log(error, errorInfo) + console.log('Cell failure: ', { + error, + errorInfo, + }) } render() { @@ -37,8 +40,8 @@ export class CellErrorBoundary extends React.Component< // in createSusepndingCell.tsx const { fallback: Fallback } = this.props - // @TODO what happens when no Fallback supplied?? - if (this.state.hasError && Fallback) { + // Fallback is guaranteed! + if (this.state.hasError) { return ( Date: Mon, 28 Aug 2023 15:34:06 +0700 Subject: [PATCH 13/20] Prettify --- packages/web/src/components/cell/isCellEmpty.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/cell/isCellEmpty.tsx b/packages/web/src/components/cell/isCellEmpty.tsx index 6443beeda882..1615c1674252 100644 --- a/packages/web/src/components/cell/isCellEmpty.tsx +++ b/packages/web/src/components/cell/isCellEmpty.tsx @@ -1,4 +1,4 @@ -import { DataObject } from './cellTypes'; +import { DataObject } from './cellTypes' /** * The default `isEmpty` implementation. Checks if any of the field is `null` or an empty array. @@ -39,10 +39,10 @@ import { DataObject } from './cellTypes'; * ``` */ function isFieldEmptyArray(field: unknown) { - return Array.isArray(field) && field.length === 0; + return Array.isArray(field) && field.length === 0 } export function isDataEmpty(data: DataObject) { return Object.values(data).every((fieldValue) => { - return fieldValue === null || isFieldEmptyArray(fieldValue); - }); + return fieldValue === null || isFieldEmptyArray(fieldValue) + }) } From 9c7dc8ee25fd359fa8413482d96d11c36f9cb705 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 28 Aug 2023 15:42:53 +0700 Subject: [PATCH 14/20] Hooks provider types --- packages/web/src/components/GraphQLHooksProvider.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/web/src/components/GraphQLHooksProvider.tsx b/packages/web/src/components/GraphQLHooksProvider.tsx index a501b2d5e9ad..45dfc5ff6643 100644 --- a/packages/web/src/components/GraphQLHooksProvider.tsx +++ b/packages/web/src/components/GraphQLHooksProvider.tsx @@ -1,6 +1,5 @@ import type { OperationVariables, - SuspenseQueryHookOptions, useBackgroundQuery as apolloUseBackgroundQuery, useReadQuery as apolloUseReadQuery, } from '@apollo/client' @@ -12,6 +11,8 @@ import type { DocumentNode } from 'graphql' * are overridden in packages/web/src/apollo/typeOverride.ts. This was originally so that you could bring your own gql client. * * The default (empty) types are defined in packages/web/src/global.web-auto-imports.ts + * + * Do not import types for hooks directly from Apollo here, unless it is an Apollo specific hook. */ type DefaultUseQueryType = < @@ -87,13 +88,13 @@ export const GraphQLHooksContext = React.createContext({ // These are apollo specific hooks! useBackgroundQuery: () => { throw new Error( - 'You must register a useBackgroundQuery hook via the `GraphQLHooksProvider`. Make sure you are importing the correct Apollo provider in App.tsx' + 'You must register a useBackgroundQuery hook via the `GraphQLHooksProvider`.' ) }, useReadQuery: () => { throw new Error( - 'You must register a useReadQuery hook via the `GraphQLHooksProvider`. Make sure you are importing the correct Apollo provider in App.tsx' + 'You must register a useReadQuery hook via the `GraphQLHooksProvider`.' ) }, }) @@ -191,7 +192,7 @@ export function useSuspenseQuery< TVariables extends OperationVariables = GraphQLOperationVariables >( query: DocumentNode, - options?: SuspenseQueryHookOptions + options?: GraphQLSuspenseQueryHookOptions ): SuspenseQueryOperationResult { return React.useContext(GraphQLHooksContext).useSuspenseQuery< TData, From ae32bad51514db8cc3dcd5e98d53e731f6dc5efc Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 29 Aug 2023 11:55:28 +0700 Subject: [PATCH 15/20] Fix resetting of error boundary --- .../src/components/cell/CellErrorBoundary.tsx | 28 ++++++++------ .../web/src/components/cell/cellTypes.tsx | 19 +++++++--- .../components/cell/createSuspendingCell.tsx | 38 +++++++++++-------- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/packages/web/src/components/cell/CellErrorBoundary.tsx b/packages/web/src/components/cell/CellErrorBoundary.tsx index 1b7b0c9734df..8a7a4f58bb5e 100644 --- a/packages/web/src/components/cell/CellErrorBoundary.tsx +++ b/packages/web/src/components/cell/CellErrorBoundary.tsx @@ -2,10 +2,17 @@ import React from 'react' import type { CellFailureProps } from './cellTypes' -type CellErrorBoundaryProps = { +export type FallbackProps = { + error: QueryOperationResult['error'] + resetErrorBoundary: () => void +} + +export type CellErrorBoundaryProps = { // Note that the fallback has to be an FC, not a Node // because the error comes from this component's state - fallback: React.FC + renderFallback: ( + fbProps: FallbackProps + ) => React.ReactElement children: React.ReactNode } @@ -38,18 +45,15 @@ export class CellErrorBoundary extends React.Component< render() { // The fallback is constructed with all the props required, except error and errorCode // in createSusepndingCell.tsx - const { fallback: Fallback } = this.props + const { renderFallback } = this.props - // Fallback is guaranteed! if (this.state.hasError) { - return ( - - ) + return renderFallback({ + error: this.state.error, + resetErrorBoundary: () => { + this.setState({ hasError: false, error: undefined }) + }, + }) } return this.props.children diff --git a/packages/web/src/components/cell/cellTypes.tsx b/packages/web/src/components/cell/cellTypes.tsx index 96a7678a60c6..831cd49eb1ff 100644 --- a/packages/web/src/components/cell/cellTypes.tsx +++ b/packages/web/src/components/cell/cellTypes.tsx @@ -3,7 +3,6 @@ import { ComponentProps, JSXElementConstructor } from 'react' import type { ApolloClient, NetworkStatus, - ObservableQuery, OperationVariables, QueryReference, UseBackgroundQueryResult, @@ -198,18 +197,26 @@ export type NonSuspenseCellQueryResult< // We call this queryResult in createCell, sadly a very overloaded term // This is just the extra things returned from useXQuery hooks export interface SuspenseCellQueryResult< - TData = any, - TVariables extends OperationVariables = any + _TData = any, + _TVariables extends OperationVariables = any > extends UseBackgroundQueryResult { client: ApolloClient // fetchMore & refetch come from UseBackgroundQueryResult - observable: ObservableQuery - networkStatus: NetworkStatus + + // not supplied in Error and Failure + // because it's implicit in these components, but the one edgecase is showing a different loader when refetching + networkStatus?: NetworkStatus + // Stuff not here: called: boolean // can we assume if we have a queryRef its called? + // observable: ObservableQuery // previousData?: any, + + // POLLING: Apollo team have said they are not ready to expose Polling yet // startPolling // stopPolling - // subscribeToMore + // ~~~ + + // subscribeToMore ~ returned from useSuspenseQuery. What would users use this for? // updateQuery } diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index ca680d9e81f1..4c35261c545f 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -1,14 +1,13 @@ import { Suspense } from 'react' import { QueryReference, useApolloClient } from '@apollo/client' -import type { InternalQueryReference } from '@apollo/client/react/cache/QueryReference' import { useBackgroundQuery, useReadQuery } from '../GraphQLHooksProvider' /** * This is part of how we let users swap out their GraphQL client while staying compatible with Cells. */ -import { CellErrorBoundary } from './CellErrorBoundary' +import { CellErrorBoundary, FallbackProps } from './CellErrorBoundary' import { CreateCellProps, DataObject, @@ -92,37 +91,44 @@ export function createSuspendingCell< const query = typeof QUERY === 'function' ? QUERY(options) : QUERY const [queryRef, other] = useBackgroundQuery(query, options) - // @TODO Waiting for feedback from Apollo team - const symbolToAccessQueryRef = Object.getOwnPropertySymbols(queryRef)[0] - // @ts-expect-error I know what Im doing - const __dangerouslyAccessInternalReference = queryRef[ - symbolToAccessQueryRef - ] as InternalQueryReference - const client = useApolloClient() const suspenseQueryResult: SuspenseCellQueryResult = { client, ...other, called: !!queryRef, - networkStatus: - __dangerouslyAccessInternalReference?.result?.networkStatus, - observable: __dangerouslyAccessInternalReference?.observable, } // @TODO(STREAMING) removed prerender handling here // Until we decide how/if we do prerendering - const FailureComponent = (fprops: any) => { + const FailureComponent = ({ error, resetErrorBoundary }: FallbackProps) => { if (!Failure) { - throw fprops.error + // So that it bubbles up to the nearest error boundary + throw error + } + + const queryResultWithErrorReset = { + ...suspenseQueryResult, + refetch: () => { + resetErrorBoundary() + return suspenseQueryResult.refetch?.() + }, } - return + // @TODO: Need to make sure refetch will reset the error boundary, when refetch is called + // otherwise the error will never go away + return ( + + ) } return ( - + } > From 1674e45b31d828d14c7f9b42ffcd08473403e551 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 29 Aug 2023 12:24:28 +0700 Subject: [PATCH 16/20] Cleanup --- packages/web/src/components/cell/createSuspendingCell.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/web/src/components/cell/createSuspendingCell.tsx b/packages/web/src/components/cell/createSuspendingCell.tsx index 4c35261c545f..e4574ff7b435 100644 --- a/packages/web/src/components/cell/createSuspendingCell.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.tsx @@ -116,8 +116,6 @@ export function createSuspendingCell< }, } - // @TODO: Need to make sure refetch will reset the error boundary, when refetch is called - // otherwise the error will never go away return ( Date: Tue, 29 Aug 2023 14:39:42 +0700 Subject: [PATCH 17/20] Fix broken test --- packages/web/jest.config.js | 4 ++++ packages/web/src/components/cell/createCell.test.tsx | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/web/jest.config.js b/packages/web/jest.config.js index 3498b181d8a2..ded53dea3d98 100644 --- a/packages/web/jest.config.js +++ b/packages/web/jest.config.js @@ -8,6 +8,10 @@ module.exports = { '**/*.test.+(ts|tsx|js|jsx)', '!**/__typetests__/*.+(ts|tsx|js|jsx)', ], + globals: { + // Required for code that use experimental flags + RWJS_ENV: {}, + }, }, { displayName: { diff --git a/packages/web/src/components/cell/createCell.test.tsx b/packages/web/src/components/cell/createCell.test.tsx index 3739015f6e4f..42e5095ad9ae 100644 --- a/packages/web/src/components/cell/createCell.test.tsx +++ b/packages/web/src/components/cell/createCell.test.tsx @@ -5,8 +5,9 @@ import { render, screen } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' +import { GraphQLHooksProvider } from '../GraphQLHooksProvider' + import { createCell } from './createCell' -import { GraphQLHooksProvider } from './GraphQLHooksProvider' describe('createCell', () => { beforeAll(() => { From 922a0f460193763045b5b89ff97d5b3a9b570e99 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 29 Aug 2023 15:03:57 +0700 Subject: [PATCH 18/20] Add some basic tests for createSuspendingCell --- .../cell/createSuspendingCell.test.tsx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 packages/web/src/components/cell/createSuspendingCell.test.tsx diff --git a/packages/web/src/components/cell/createSuspendingCell.test.tsx b/packages/web/src/components/cell/createSuspendingCell.test.tsx new file mode 100644 index 000000000000..4da8f1468efc --- /dev/null +++ b/packages/web/src/components/cell/createSuspendingCell.test.tsx @@ -0,0 +1,146 @@ +/** + * @jest-environment jsdom + */ + +import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev' +import { render, screen } from '@testing-library/react' +import '@testing-library/jest-dom/extend-expect' + +import { GraphQLHooksProvider } from '../GraphQLHooksProvider' + +import { createSuspendingCell } from './createSuspendingCell' + +import type { useReadQuery, useBackgroundQuery } from '@apollo/client' + +type ReadQueryHook = typeof useReadQuery +type BgQueryHook = typeof useBackgroundQuery + +jest.mock('@apollo/client', () => { + return { + useApolloClient: jest.fn(), + } +}) + +// @TODO: once we have finalised implementation, we need to add tests for +// all the other states. We would also need to figure out how to test the Suspense state. +// No point doing this now, as the implementation is in flux! + +describe('createSuspendingCell', () => { + beforeAll(() => { + globalThis.RWJS_ENV = { + RWJS_EXP_STREAMING_SSR: true, + } + loadDevMessages() + loadErrorMessages() + }) + + const mockedUseBgQuery = (() => { + return ['mocked-query-ref', { refetch: jest.fn(), fetchMore: jest.fn() }] + }) as unknown as BgQueryHook + + const mockedQueryHook = () => ({ data: {} }) + + test.only('Renders a static Success component', async () => { + const TestCell = createSuspendingCell({ + // @ts-expect-error - Purposefully using a plain string here. + QUERY: 'query TestQuery { answer }', + Success: () => <>Great success!, + }) + + render( + + + + ) + screen.getByText(/^Great success!$/) + }) + + test.only('Renders Success with data', async () => { + const TestCell = createSuspendingCell({ + // @ts-expect-error - Purposefully using a plain string here. + QUERY: 'query TestQuery { answer }', + Success: ({ answer }) => ( + <> +
+
What's the meaning of life?
+
{answer}
+
+ + ), + }) + + const myUseQueryHook = (() => { + return { data: { answer: 42 } } + }) as unknown as ReadQueryHook + + render( + + + + ) + + screen.getByText(/^What's the meaning of life\?$/) + screen.getByText(/^42$/) + }) + + test.only('Renders Success if any of the fields have data (i.e. not just the first)', async () => { + const TestCell = createSuspendingCell({ + // @ts-expect-error - Purposefully using a plain string here. + QUERY: 'query TestQuery { users { name } posts { title } }', + Empty: () => <>No users or posts, + Success: ({ users, posts }) => ( + <> +
+ {users.length > 0 ? ( +
    + {users.map(({ name }) => ( +
  • {name}
  • + ))} +
+ ) : ( + 'no users' + )} +
+
+ {posts.length > 0 ? ( +
    + {posts.map(({ title }) => ( +
  • {title}
  • + ))} +
+ ) : ( + 'no posts' + )} +
+ + ), + }) + + const myReadQueryHook = (() => { + return { + data: { + users: [], + posts: [{ title: 'bazinga' }, { title: 'kittens' }], + }, + } + }) as unknown as ReadQueryHook + + render( + + + + ) + + screen.getByText(/bazinga/) + screen.getByText(/kittens/) + }) +}) From 00c5329c09b22ed73ef323e51c126c4fbeac2df4 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 29 Aug 2023 15:36:48 +0700 Subject: [PATCH 19/20] Update comments in cellTypes --- .../web/src/components/cell/cellTypes.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/web/src/components/cell/cellTypes.tsx b/packages/web/src/components/cell/cellTypes.tsx index 831cd49eb1ff..fa352cc3805d 100644 --- a/packages/web/src/components/cell/cellTypes.tsx +++ b/packages/web/src/components/cell/cellTypes.tsx @@ -206,17 +206,20 @@ export interface SuspenseCellQueryResult< // not supplied in Error and Failure // because it's implicit in these components, but the one edgecase is showing a different loader when refetching networkStatus?: NetworkStatus + called: boolean // can we assume if we have a queryRef its called? // Stuff not here: - called: boolean // can we assume if we have a queryRef its called? // observable: ObservableQuery - // previousData?: any, - - // POLLING: Apollo team have said they are not ready to expose Polling yet - // startPolling - // stopPolling + // previousData?: TData, May not be relevant anymore. + + // ObservableQueryFields 👇 + // subscribeToMore ~ returned from useSuspenseQuery. What would users use this for? + // updateQuery + // refetch + // reobserve + // variables <~ variables passed to the query. Startup club have reported using this, but why? + // fetchMore + // startPolling <~ Apollo team are not ready to expose Polling yet + // stopPolling // ~~~ - - // subscribeToMore ~ returned from useSuspenseQuery. What would users use this for? - // updateQuery } From ba290f7598225b6aae2e52f2b21b838b0520c3a8 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 29 Aug 2023 23:08:34 +0700 Subject: [PATCH 20/20] Silly lint --- .../web/src/components/cell/createSuspendingCell.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/web/src/components/cell/createSuspendingCell.test.tsx b/packages/web/src/components/cell/createSuspendingCell.test.tsx index 4da8f1468efc..749636fd3d2c 100644 --- a/packages/web/src/components/cell/createSuspendingCell.test.tsx +++ b/packages/web/src/components/cell/createSuspendingCell.test.tsx @@ -1,7 +1,7 @@ /** * @jest-environment jsdom */ - +import type { useReadQuery, useBackgroundQuery } from '@apollo/client' import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev' import { render, screen } from '@testing-library/react' import '@testing-library/jest-dom/extend-expect' @@ -10,8 +10,6 @@ import { GraphQLHooksProvider } from '../GraphQLHooksProvider' import { createSuspendingCell } from './createSuspendingCell' -import type { useReadQuery, useBackgroundQuery } from '@apollo/client' - type ReadQueryHook = typeof useReadQuery type BgQueryHook = typeof useBackgroundQuery