Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: React Suspense support for useFeed #940

Merged
merged 1 commit into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
131 changes: 92 additions & 39 deletions packages/react/src/discovery/useFeed.ts
Original file line number Diff line number Diff line change
@@ -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<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>
* );
* ```
*
* @experimental This API can change without notice
* @category Discovery
* @group Hooks
* @param args - {@link UseSuspenseFeedArgs}
*/
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 };
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simpler is better.


/**
* @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
20 changes: 13 additions & 7 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 @@ -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<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
Loading