Skip to content

Commit

Permalink
feat: React Suspense support for useFeed
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarenaldi committed May 14, 2024
1 parent 21f652d commit 5b5b0ff
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 145 deletions.
7 changes: 7 additions & 0 deletions .changeset/soft-yaks-shop.md
Original file line number Diff line number Diff line change
@@ -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
13 changes: 1 addition & 12 deletions examples/web/src/components/auth/WhenLoggedIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Session> = (session: T) => ReactNode;

export type LoggedInChildren<T extends Session> = ReactNode | RenderFunction<T>;
Expand All @@ -30,15 +27,7 @@ export function WhenLoggedIn<
T extends SessionType.JustWallet | SessionType.WithProfile,
S extends WalletOnlySession | ProfileSession,
>(props: WhenLoggedInProps<T, S>) {
const { data: session, loading, error } = useSession();

if (loading) {
return <Loading />;
}

if (error) {
return <ErrorMessage error={error} />;
}
const { data: session } = useSession({ suspense: true });

if (session.type !== props.with) {
return props.fallback ?? null;
Expand Down
36 changes: 11 additions & 25 deletions examples/web/src/discovery/UseFeed.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{data?.length === 0 && <p>No items</p>}

{loading && <Loading />}

{error && <ErrorMessage error={error} />}
<h1>
<code>useFeed</code>
</h1>

<button disabled={loading || beforeCount === 0} onClick={prev}>
Fetch newer
</button>
{data?.length === 0 && <p>No items</p>}

{data?.map((item, i) => (
<PublicationCard key={`${item.root.id}-${i}`} publication={item.root} />
Expand All @@ -38,14 +30,8 @@ function UseFeedInner({ profileId }: { profileId: ProfileId }) {

export function UseFeed() {
return (
<div>
<h1>
<code>useFeed</code>
</h1>

<RequireProfileSession message="Log in to view this example.">
{({ profile }) => <UseFeedInner profileId={profile.id} />}
</RequireProfileSession>
</div>
<RequireProfileSession message="Log in to view this example.">
{() => <Feed />}
</RequireProfileSession>
);
}
12 changes: 6 additions & 6 deletions packages/react/src/authentication/useSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export type Session = AnonymousSession | ProfileSession | WalletOnlySession;
/**
* {@link useSession} hook arguments
*/
export type UseSessionArgs<TSuspense extends boolean> = SuspenseEnabled<TSuspense>;
export type UseSessionArgs = SuspenseEnabled;

/**
* Returns current {@link Session} data.
Expand Down Expand Up @@ -125,7 +125,7 @@ export type UseSessionArgs<TSuspense extends boolean> = SuspenseEnabled<TSuspens
* @category Authentication
* @group Hooks
*/
export function useSession(args: UseSessionArgs<true>): SuspenseResult<Session>;
export function useSession(args: UseSessionArgs): SuspenseResult<Session>;

/**
* Returns current {@link Session} data.
Expand Down Expand Up @@ -160,11 +160,11 @@ export function useSession(args: UseSessionArgs<true>): SuspenseResult<Session>;
* @category Authentication
* @group Hooks
*/
export function useSession(args?: UseSessionArgs<never>): ReadResult<Session, UnspecifiedError>;
export function useSession(): ReadResult<Session, UnspecifiedError>;

export function useSession(
args?: UseSessionArgs<boolean>,
): ReadResult<Session, UnspecifiedError> | SuspenseResult<Session> {
export function useSession(args?: {
suspense: boolean;
}): ReadResult<Session, UnspecifiedError> | SuspenseResult<Session> {
const sessionData = useSessionDataVar();

const [primeCacheWithProfile, data] = useProfileFromCache(sessionData);
Expand Down
130 changes: 91 additions & 39 deletions packages/react/src/discovery/useFeed.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,112 @@
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<FeedRequest>;

export type { FeedRequest, FeedWhere };

/**
* {@link useFeed} hook arguments with Suspense support
*
* @experimental This API can change without notice
*/
export type UseSuspenseFeedArgs = SuspenseEnabled<UseFeedArgs>;

/**
* 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 <div>Loading...</div>;
*
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <ul>
* {data.map((item, idx) => (
* <li key={`${item.root.id}-${idx}`}>
* // render item details
* </li>
* ))}
* </ul>
* );
* ```
*
* @category Discovery
* @group Hooks
* @param args - {@link UseFeedArgs}
*/
export function useFeed({ where }: UseFeedArgs): PaginatedReadResult<FeedItem[]>;

/**
* 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 <div>Loading...</div>;
*
* if (error) return <div>Error: {error.message}</div>;
*
* return (
* <ul>
* {data.map((item, idx) => (
* <li key={`${item.root.id}-${idx}`}>
* // render item details
* </li>
* ))}
* </ul>
* );
* }
* const { data, loading, error } = useFeed({
* where: {
* for: '0x01`, // profileId
* },
* suspense: true,
* });
*
* return (
* <ul>
* {data.map((item, idx) => (
* <li key={`${item.root.id}-${idx}`}>
* // render item details
* </li>
* ))}
* </ul>
* );
* ```
*
* @category Discovery
* @group Hooks
* @param args - {@link UseFeedArgs}
*/
export function useFeed({ where }: UseFeedArgs): PaginatedReadResult<FeedItem[]> {
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<FeedItem[]>;

export function useFeed({
suspense = false,
where,
}: UseFeedArgs & { suspense?: boolean }): SuspendablePaginatedResult<FeedItem[]> {
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,
}),
});
}
16 changes: 1 addition & 15 deletions packages/react/src/helpers/reads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,20 +110,7 @@ export function useReadResult<
return buildReadResult(data?.result, error);
}

export type OmitCursor<T> = Omit<T, 'cursor'>;

export type PaginatedArgs<T> = 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<T> = Prettify<Omit<T, 'cursor'>>;

/**
* A paginated read result.
Expand Down
5 changes: 2 additions & 3 deletions packages/react/src/helpers/suspense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ export type SuspenseReadResult<T, E = never> = SuspenseResultWithError<T, E>;
*
* @experimental This is an experimental type that can change at any time.
*/
export type SuspenseEnabled<TSuspense extends boolean> = {
suspense?: TSuspense;
};
// eslint-disable-next-line @typescript-eslint/ban-types
export type SuspenseEnabled<T = {}> = T & { suspense: true };

/**
* @internal
Expand Down
2 changes: 0 additions & 2 deletions packages/react/src/misc/useLatestPaidActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,13 @@ export type UseLatestPaidActionsArgs = PaginatedArgs<LatestPaidActionRequest>;
export function useLatestPaidActions({
filter,
where,
limit,
}: UseLatestPaidActionsArgs = {}): PaginatedReadResult<AnyPaidAction[]> {
return usePaginatedReadResult(
useLatestPaidActionsBase(
useLensApolloClient({
variables: useFragmentVariables({
filter,
where,
limit,
}),
}),
),
Expand Down
18 changes: 12 additions & 6 deletions packages/react/src/profile/useProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>) {
function profileNotFound({ forProfileId, forHandle }: UseProfileArgs) {
return new NotFoundError(
forProfileId
? `Profile with id: ${forProfileId}`
Expand All @@ -26,8 +26,14 @@ export type { ProfileRequest };
/**
* {@link useProfile} hook arguments
*/
export type UseProfileArgs<TSuspense extends boolean = never> = OneOf<ProfileRequest> &
SuspenseEnabled<TSuspense>;
export type UseProfileArgs = OneOf<ProfileRequest>;

/**
* {@link useProfile} hook arguments with Suspense support
*
* @experimental This API can change without notice
*/
export type UseSuspenseProfileArgs = SuspenseEnabled<UseProfileArgs>;

export type UseProfileResult =
| ReadResult<Profile, NotFoundError | UnspecifiedError>
Expand Down Expand Up @@ -61,7 +67,7 @@ export type UseProfileResult =
export function useProfile({
forHandle,
forProfileId,
}: UseProfileArgs<never>): ReadResult<Profile, NotFoundError | UnspecifiedError>;
}: UseProfileArgs): ReadResult<Profile, NotFoundError | UnspecifiedError>;

/**
* Fetches a Profile by either its full handle or id.
Expand All @@ -82,13 +88,13 @@ export function useProfile({
* @param args - {@link UseProfileArgs}
*/
export function useProfile(
args: UseProfileArgs<true>,
args: UseSuspenseProfileArgs,
): SuspenseResultWithError<Profile, NotFoundError>;

export function useProfile({
suspense = false,
...request
}: UseProfileArgs<boolean>): UseProfileResult {
}: UseProfileArgs & { suspense?: boolean }): UseProfileResult {
invariant(
request.forProfileId === undefined || request.forHandle === undefined,
"Only one of 'forProfileId' or 'forHandle' should be provided to 'useProfile' hook",
Expand Down
Loading

0 comments on commit 5b5b0ff

Please sign in to comment.