From 46a648de26c062ccd3b7bc5561e897900ce9df48 Mon Sep 17 00:00:00 2001 From: Cesare Naldi Date: Tue, 14 May 2024 18:15:30 +0200 Subject: [PATCH] feat: React Suspense support for useFeed --- .changeset/soft-yaks-shop.md | 7 + .../web/src/components/auth/WhenLoggedIn.tsx | 13 +- examples/web/src/discovery/UseFeed.tsx | 36 ++--- .../react/src/authentication/useSession.ts | 12 +- packages/react/src/discovery/useFeed.ts | 131 ++++++++++++------ packages/react/src/helpers/reads.ts | 16 +-- packages/react/src/helpers/suspense.ts | 5 +- .../react/src/misc/useLatestPaidActions.ts | 2 - packages/react/src/profile/useProfile.ts | 20 ++- .../react/src/publication/usePublication.ts | 58 ++++---- .../react/src/publication/usePublications.ts | 23 +-- 11 files changed, 173 insertions(+), 150 deletions(-) create mode 100644 .changeset/soft-yaks-shop.md diff --git a/.changeset/soft-yaks-shop.md b/.changeset/soft-yaks-shop.md new file mode 100644 index 0000000000..d249abf2c6 --- /dev/null +++ b/.changeset/soft-yaks-shop.md @@ -0,0 +1,7 @@ +--- +"@lens-protocol/react": patch +"@lens-protocol/react-native": patch +"@lens-protocol/react-web": patch +--- + +**feat:** add React Suspense support to `useFeed` hook diff --git a/examples/web/src/components/auth/WhenLoggedIn.tsx b/examples/web/src/components/auth/WhenLoggedIn.tsx index 584d60c196..2bd4897f57 100644 --- a/examples/web/src/components/auth/WhenLoggedIn.tsx +++ b/examples/web/src/components/auth/WhenLoggedIn.tsx @@ -7,9 +7,6 @@ import { } from '@lens-protocol/react-web'; import { ReactNode } from 'react'; -import { ErrorMessage } from '../error/ErrorMessage'; -import { Loading } from '../loading/Loading'; - export type RenderFunction = (session: T) => ReactNode; export type LoggedInChildren = ReactNode | RenderFunction; @@ -30,15 +27,7 @@ export function WhenLoggedIn< T extends SessionType.JustWallet | SessionType.WithProfile, S extends WalletOnlySession | ProfileSession, >(props: WhenLoggedInProps) { - const { data: session, loading, error } = useSession(); - - if (loading) { - return ; - } - - if (error) { - return ; - } + const { data: session } = useSession({ suspense: true }); if (session.type !== props.with) { return props.fallback ?? null; diff --git a/examples/web/src/discovery/UseFeed.tsx b/examples/web/src/discovery/UseFeed.tsx index 8157c10b3b..ed1c6cf6cf 100644 --- a/examples/web/src/discovery/UseFeed.tsx +++ b/examples/web/src/discovery/UseFeed.tsx @@ -1,31 +1,23 @@ -import { ProfileId, useFeed } from '@lens-protocol/react-web'; +import { useFeed } from '@lens-protocol/react-web'; import { RequireProfileSession } from '../components/auth'; import { PublicationCard } from '../components/cards'; -import { ErrorMessage } from '../components/error/ErrorMessage'; -import { Loading } from '../components/loading/Loading'; import { useInfiniteScroll } from '../hooks/useInfiniteScroll'; -function UseFeedInner({ profileId }: { profileId: ProfileId }) { - const { data, error, loading, hasMore, beforeCount, observeRef, prev } = useInfiniteScroll( +function Feed() { + const { data, hasMore, observeRef } = useInfiniteScroll( useFeed({ - where: { - for: profileId, - }, + suspense: true, }), ); return (
- {data?.length === 0 &&

No items

} - - {loading && } - - {error && } +

+ useFeed +

- + {data?.length === 0 &&

No items

} {data?.map((item, i) => ( @@ -38,14 +30,8 @@ function UseFeedInner({ profileId }: { profileId: ProfileId }) { export function UseFeed() { return ( -
-

- useFeed -

- - - {({ profile }) => } - -
+ + {() => } + ); } diff --git a/packages/react/src/authentication/useSession.ts b/packages/react/src/authentication/useSession.ts index 92a68a7150..976e8dc992 100644 --- a/packages/react/src/authentication/useSession.ts +++ b/packages/react/src/authentication/useSession.ts @@ -89,7 +89,7 @@ export type Session = AnonymousSession | ProfileSession | WalletOnlySession; /** * {@link useSession} hook arguments */ -export type UseSessionArgs = SuspenseEnabled; +export type UseSessionArgs = SuspenseEnabled; /** * Returns current {@link Session} data. @@ -125,7 +125,7 @@ export type UseSessionArgs = SuspenseEnabled): SuspenseResult; +export function useSession(args: UseSessionArgs): SuspenseResult; /** * Returns current {@link Session} data. @@ -160,11 +160,11 @@ export function useSession(args: UseSessionArgs): SuspenseResult; * @category Authentication * @group Hooks */ -export function useSession(args?: UseSessionArgs): ReadResult; +export function useSession(): ReadResult; -export function useSession( - args?: UseSessionArgs, -): ReadResult | SuspenseResult { +export function useSession(args?: { + suspense: boolean; +}): ReadResult | SuspenseResult { const sessionData = useSessionDataVar(); const [primeCacheWithProfile, data] = useProfileFromCache(sessionData); diff --git a/packages/react/src/discovery/useFeed.ts b/packages/react/src/discovery/useFeed.ts index 0242833469..a658e075e0 100644 --- a/packages/react/src/discovery/useFeed.ts +++ b/packages/react/src/discovery/useFeed.ts @@ -1,60 +1,113 @@ -import { FeedItem, FeedRequest, useFeed as useBaseFeedQuery } from '@lens-protocol/api-bindings'; +import { FeedDocument, FeedItem, FeedRequest, FeedWhere } from '@lens-protocol/api-bindings'; -import { SessionType, useSession } from '../authentication'; +import { SessionType, UseSessionArgs, useSession } from '../authentication'; import { useLensApolloClient } from '../helpers/arguments'; -import { PaginatedArgs, PaginatedReadResult, usePaginatedReadResult } from '../helpers/reads'; +import { PaginatedArgs, PaginatedReadResult } from '../helpers/reads'; +import { + SuspendablePaginatedResult, + SuspenseEnabled, + SuspensePaginatedResult, + useSuspendablePaginatedQuery, +} from '../helpers/suspense'; import { useFragmentVariables } from '../helpers/variables'; +/** + * {@link useFeed} hook arguments + */ export type UseFeedArgs = PaginatedArgs; +export type { FeedRequest, FeedWhere }; + +/** + * {@link useFeed} hook arguments with Suspense support + * + * @experimental This API can change without notice + */ +export type UseSuspenseFeedArgs = SuspenseEnabled; + /** * Fetch a the feed of a given profile and filters. * * You MUST be authenticated via {@link useLogin} to use this hook. * + * @example + * ```tsx + * const { data, loading, error } = useFeed({ + * where: { + * for: '0x01`, // profileId + * }, + * }); + * + * if (loading) return
Loading...
; + * + * if (error) return
Error: {error.message}
; + * + * return ( + *
    + * {data.map((item, idx) => ( + *
  • + * // render item details + *
  • + * ))} + *
+ * ); + * ``` + * * @category Discovery * @group Hooks * @param args - {@link UseFeedArgs} + */ +export function useFeed({ where }: UseFeedArgs): PaginatedReadResult; + +/** + * Fetch a the feed of a given profile and filters. + * + * You MUST be authenticated via {@link useLogin} to use this hook. + * + * This signature supports [React Suspense](https://react.dev/reference/react/Suspense). * * @example * ```tsx - * import { useFeed, ProfileId } from '@lens-protocol/react'; - * - * function Feed({ profileId }: { profileId: ProfileId }) { - * const { data, loading, error } = useFeed({ - * where: { - * for: profileId, - * }, - * }); - * - * if (loading) return
Loading...
; - * - * if (error) return
Error: {error.message}
; - * - * return ( - *
    - * {data.map((item, idx) => ( - *
  • - * // render item details - *
  • - * ))} - *
- * ); - * } + * const { data, loading, error } = useFeed({ + * where: { + * for: '0x01`, // profileId + * }, + * suspense: true, + * }); + * + * return ( + *
    + * {data.map((item, idx) => ( + *
  • + * // render item details + *
  • + * ))} + *
+ * ); * ``` + * + * @experimental This API can change without notice + * @category Discovery + * @group Hooks + * @param args - {@link UseSuspenseFeedArgs} */ -export function useFeed({ where }: UseFeedArgs): PaginatedReadResult { - const { data: session } = useSession(); - - return usePaginatedReadResult( - useBaseFeedQuery( - useLensApolloClient({ - variables: useFragmentVariables({ - where, - statsFor: where?.metadata?.publishedOn, - }), - skip: session?.type !== SessionType.WithProfile, +export function useFeed({ where }: UseSuspenseFeedArgs): SuspensePaginatedResult; + +export function useFeed({ + suspense = false, + where, +}: UseFeedArgs & { suspense?: boolean }): SuspendablePaginatedResult { + const { data: session } = useSession({ suspense } as UseSessionArgs); + + return useSuspendablePaginatedQuery({ + suspense, + query: FeedDocument, + options: useLensApolloClient({ + variables: useFragmentVariables({ + where, + statsFor: where?.metadata?.publishedOn, }), - ), - ); + skip: session.type !== SessionType.WithProfile, + }), + }); } diff --git a/packages/react/src/helpers/reads.ts b/packages/react/src/helpers/reads.ts index 8a8a7b6bbf..a9ef4b1cd6 100644 --- a/packages/react/src/helpers/reads.ts +++ b/packages/react/src/helpers/reads.ts @@ -12,7 +12,6 @@ import { InputMaybe, Cursor, PaginatedResultInfo, - LimitType, } from '@lens-protocol/api-bindings'; import { Prettify } from '@lens-protocol/shared-kernel'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -111,20 +110,7 @@ export function useReadResult< return buildReadResult(data?.result, error); } -export type OmitCursor = Omit; - -export type PaginatedArgs = Prettify< - OmitCursor< - T & { - /** - * The number of items to return. - * - * @defaultValue Default value is set by the API and it might differ between queries. - */ - limit?: LimitType; - } - > ->; +export type PaginatedArgs = Prettify>; /** * A paginated read result. diff --git a/packages/react/src/helpers/suspense.ts b/packages/react/src/helpers/suspense.ts index 5d702fb3a0..bbd5712aa9 100644 --- a/packages/react/src/helpers/suspense.ts +++ b/packages/react/src/helpers/suspense.ts @@ -51,9 +51,8 @@ export type SuspenseReadResult = SuspenseResultWithError; * * @experimental This is an experimental type that can change at any time. */ -export type SuspenseEnabled = { - suspense?: TSuspense; -}; +// eslint-disable-next-line @typescript-eslint/ban-types +export type SuspenseEnabled = T & { suspense: true }; /** * @internal diff --git a/packages/react/src/misc/useLatestPaidActions.ts b/packages/react/src/misc/useLatestPaidActions.ts index 75671f9d3b..462864eeed 100644 --- a/packages/react/src/misc/useLatestPaidActions.ts +++ b/packages/react/src/misc/useLatestPaidActions.ts @@ -43,7 +43,6 @@ export type UseLatestPaidActionsArgs = PaginatedArgs; export function useLatestPaidActions({ filter, where, - limit, }: UseLatestPaidActionsArgs = {}): PaginatedReadResult { return usePaginatedReadResult( useLatestPaidActionsBase( @@ -51,7 +50,6 @@ export function useLatestPaidActions({ variables: useFragmentVariables({ filter, where, - limit, }), }), ), diff --git a/packages/react/src/profile/useProfile.ts b/packages/react/src/profile/useProfile.ts index 923c2e4096..41c4129022 100644 --- a/packages/react/src/profile/useProfile.ts +++ b/packages/react/src/profile/useProfile.ts @@ -13,7 +13,7 @@ import { ReadResult } from '../helpers/reads'; import { SuspenseEnabled, SuspenseResultWithError, useSuspendableQuery } from '../helpers/suspense'; import { useFragmentVariables } from '../helpers/variables'; -function profileNotFound({ forProfileId, forHandle }: UseProfileArgs) { +function profileNotFound({ forProfileId, forHandle }: UseProfileArgs) { return new NotFoundError( forProfileId ? `Profile with id: ${forProfileId}` @@ -26,8 +26,14 @@ export type { ProfileRequest }; /** * {@link useProfile} hook arguments */ -export type UseProfileArgs = OneOf & - SuspenseEnabled; +export type UseProfileArgs = OneOf; + +/** + * {@link useProfile} hook arguments with Suspense support + * + * @experimental This API can change without notice + */ +export type UseSuspenseProfileArgs = SuspenseEnabled; export type UseProfileResult = | ReadResult @@ -61,7 +67,7 @@ export type UseProfileResult = export function useProfile({ forHandle, forProfileId, -}: UseProfileArgs): ReadResult; +}: UseProfileArgs): ReadResult; /** * Fetches a Profile by either its full handle or id. @@ -77,18 +83,18 @@ export function useProfile({ * console.log(data.id); * ``` * + * @experimental This API can change without notice * @category Profiles * @group Hooks - * @param args - {@link UseProfileArgs} */ export function useProfile( - args: UseProfileArgs, + args: UseSuspenseProfileArgs, ): SuspenseResultWithError; export function useProfile({ suspense = false, ...request -}: UseProfileArgs): UseProfileResult { +}: UseProfileArgs & { suspense?: boolean }): UseProfileResult { invariant( request.forProfileId === undefined || request.forHandle === undefined, "Only one of 'forProfileId' or 'forHandle' should be provided to 'useProfile' hook", diff --git a/packages/react/src/publication/usePublication.ts b/packages/react/src/publication/usePublication.ts index 0c361b60c1..8db21f7cba 100644 --- a/packages/react/src/publication/usePublication.ts +++ b/packages/react/src/publication/usePublication.ts @@ -13,7 +13,7 @@ import { ReadResult } from '../helpers/reads'; import { SuspenseEnabled, SuspenseResultWithError, useSuspendableQuery } from '../helpers/suspense'; import { useFragmentVariables } from '../helpers/variables'; -function publicationNotFound({ forId, forTxHash }: UsePublicationArgs) { +function publicationNotFound({ forId, forTxHash }: UsePublicationArgs) { return new NotFoundError( forId ? `Publication with id ${forId} was not found` @@ -26,8 +26,14 @@ export type { PublicationRequest }; /** * {@link usePublication} hook arguments */ -export type UsePublicationArgs = OneOf & - SuspenseEnabled; +export type UsePublicationArgs = OneOf; + +/** + * {@link usePublication} hook arguments with Suspense support + * + * @experimental This API can change without notice + */ +export type UseSuspensePublicationArgs = SuspenseEnabled; export type UsePublicationResult = | ReadResult @@ -36,34 +42,28 @@ export type UsePublicationResult = /** * Fetch a publication by either its publication id or transaction hash. * - * @example - * ```tsx - * const { data, error, loading } = usePublication({ - * forId: '0x04-0x0b', - * }); - * ``` - * - * ## Basic Usage - * - * Get Publication by Id: - * * ```ts * const { data, error, loading } = usePublication({ * forId: '0x04-0x0b', - * }); - * ``` - * - * Get Publication by Transaction Hash: - * - * ```ts - * const { data, error, loading } = usePublication({ + * // OR * forTxHash: '0xcd0655e8d1d131ebfc72fa5ebff6ed0430e6e39e729af1a81da3b6f33822a6ff', * }); * ``` * - * ## Suspense Enabled + * @category Publications + * @group Hooks * - * You can enable suspense mode to suspend the component until the session data is available. + * @param args - {@link UsePublicationArgs} + */ +export function usePublication({ + forId, + forTxHash, +}: UsePublicationArgs): ReadResult; + +/** + * Fetch a publication by either its publication id or transaction hash. + * + * This signature supports [React Suspense](https://react.dev/reference/react/Suspense). * * ```ts * const { data } = usePublication({ @@ -74,22 +74,18 @@ export type UsePublicationResult = * console.log(data.id); * ``` * + * @experimental This API can change without notice * @category Publications * @group Hooks - * - * @param args - {@link UsePublicationArgs} */ -export function usePublication({ - forId, - forTxHash, -}: UsePublicationArgs): ReadResult; export function usePublication( - args: UsePublicationArgs, + args: UseSuspensePublicationArgs, ): SuspenseResultWithError; + export function usePublication({ suspense = false, ...request -}: UsePublicationArgs): UsePublicationResult { +}: UsePublicationArgs & { suspense?: boolean }): UsePublicationResult { invariant( request.forId === undefined || request.forTxHash === undefined, "Only one of 'forId' or 'forTxHash' should be provided to 'usePublication' hook", diff --git a/packages/react/src/publication/usePublications.ts b/packages/react/src/publication/usePublications.ts index f50d84e377..d28518639d 100644 --- a/packages/react/src/publication/usePublications.ts +++ b/packages/react/src/publication/usePublications.ts @@ -17,11 +17,17 @@ import { useFragmentVariables } from '../helpers/variables'; /** * {@link usePublications} hook arguments */ -export type UsePublicationsArgs = - PaginatedArgs & SuspenseEnabled; +export type UsePublicationsArgs = PaginatedArgs; export type { PublicationsRequest }; +/** + * {@link usePublications} hook arguments with Suspense support + * + * @experimental This API can change without notice + */ +export type UseSuspensePublicationsArgs = SuspenseEnabled; + /** * Retrieves a paginated list of publications, filtered according to specified criteria. * @@ -68,11 +74,8 @@ export type { PublicationsRequest }; * * @category Publications * @group Hooks - * @param args - {@link UsePublicationsArgs} */ -export function usePublications( - args: UsePublicationsArgs, -): PaginatedReadResult; +export function usePublications(args: UsePublicationsArgs): PaginatedReadResult; /** * Retrieves a paginated list of publications, filtered according to specified criteria. * @@ -85,19 +88,19 @@ export function usePublications( * }); * ``` * + * @experimental This API can change without notice * @category Publications * @group Hooks - * @param args - {@link UsePublicationsArgs} */ export function usePublications( - args: UsePublicationsArgs, + args: UseSuspensePublicationsArgs, ): SuspensePaginatedResult; export function usePublications({ + limit, suspense = false, where, - limit, -}: UsePublicationsArgs): SuspendablePaginatedResult { +}: UsePublicationsArgs & { suspense?: boolean }): SuspendablePaginatedResult { return useSuspendablePaginatedQuery({ suspense, query: PublicationsDocument,