Skip to content
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
6 changes: 6 additions & 0 deletions .changeset/beige-lemons-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphprotocol/hypergraph-react": patch
---

add useEntitiesPublicInfinite hook]

Copilot AI Nov 14, 2025

Copy link

Choose a reason for hiding this comment

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

The changeset description has a typo - there's an extra closing bracket ] at the end of "add useEntitiesPublicInfinite hook]".

Suggested change
add useEntitiesPublicInfinite hook]
add useEntitiesPublicInfinite hook

Copilot uses AI. Check for mistakes.

23 changes: 23 additions & 0 deletions apps/events/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@ import { Route as SpaceSpaceIdPlaygroundRouteImport } from './routes/space/$spac
import { Route as SpaceSpaceIdEventsRouteImport } from './routes/space/$spaceId/events'
import { Route as SpaceSpaceIdChatRouteImport } from './routes/space/$spaceId/chat'

const PodcastsInfiniteLazyRouteImport = createFileRoute('/podcasts-infinite')()
const PodcastsLazyRouteImport = createFileRoute('/podcasts')()
const PlaygroundLazyRouteImport = createFileRoute('/playground')()
const LoginLazyRouteImport = createFileRoute('/login')()

const PodcastsInfiniteLazyRoute = PodcastsInfiniteLazyRouteImport.update({
id: '/podcasts-infinite',
path: '/podcasts-infinite',
getParentRoute: () => rootRouteImport,
} as any).lazy(() =>
import('./routes/podcasts-infinite.lazy').then((d) => d.Route),
)
const PodcastsLazyRoute = PodcastsLazyRouteImport.update({
id: '/podcasts',
path: '/podcasts',
Expand Down Expand Up @@ -105,6 +113,7 @@ export interface FileRoutesByFullPath {
'/login': typeof LoginLazyRoute
'/playground': typeof PlaygroundLazyRoute
'/podcasts': typeof PodcastsLazyRoute
'/podcasts-infinite': typeof PodcastsInfiniteLazyRoute
'/account-inbox/$inboxId': typeof AccountInboxInboxIdRoute
'/friends/$accountAddress': typeof FriendsAccountAddressRoute
'/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren
Expand All @@ -121,6 +130,7 @@ export interface FileRoutesByTo {
'/login': typeof LoginLazyRoute
'/playground': typeof PlaygroundLazyRoute
'/podcasts': typeof PodcastsLazyRoute
'/podcasts-infinite': typeof PodcastsInfiniteLazyRoute
'/account-inbox/$inboxId': typeof AccountInboxInboxIdRoute
'/friends/$accountAddress': typeof FriendsAccountAddressRoute
'/space/$spaceId/chat': typeof SpaceSpaceIdChatRoute
Expand All @@ -137,6 +147,7 @@ export interface FileRoutesById {
'/login': typeof LoginLazyRoute
'/playground': typeof PlaygroundLazyRoute
'/podcasts': typeof PodcastsLazyRoute
'/podcasts-infinite': typeof PodcastsInfiniteLazyRoute
'/account-inbox/$inboxId': typeof AccountInboxInboxIdRoute
'/friends/$accountAddress': typeof FriendsAccountAddressRoute
'/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren
Expand All @@ -155,6 +166,7 @@ export interface FileRouteTypes {
| '/login'
| '/playground'
| '/podcasts'
| '/podcasts-infinite'
| '/account-inbox/$inboxId'
| '/friends/$accountAddress'
| '/space/$spaceId'
Expand All @@ -171,6 +183,7 @@ export interface FileRouteTypes {
| '/login'
| '/playground'
| '/podcasts'
| '/podcasts-infinite'
| '/account-inbox/$inboxId'
| '/friends/$accountAddress'
| '/space/$spaceId/chat'
Expand All @@ -186,6 +199,7 @@ export interface FileRouteTypes {
| '/login'
| '/playground'
| '/podcasts'
| '/podcasts-infinite'
| '/account-inbox/$inboxId'
| '/friends/$accountAddress'
| '/space/$spaceId'
Expand All @@ -203,13 +217,21 @@ export interface RootRouteChildren {
LoginLazyRoute: typeof LoginLazyRoute
PlaygroundLazyRoute: typeof PlaygroundLazyRoute
PodcastsLazyRoute: typeof PodcastsLazyRoute
PodcastsInfiniteLazyRoute: typeof PodcastsInfiniteLazyRoute
AccountInboxInboxIdRoute: typeof AccountInboxInboxIdRoute
FriendsAccountAddressRoute: typeof FriendsAccountAddressRoute
SpaceSpaceIdRoute: typeof SpaceSpaceIdRouteWithChildren
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/podcasts-infinite': {
id: '/podcasts-infinite'
path: '/podcasts-infinite'
fullPath: '/podcasts-infinite'
preLoaderRoute: typeof PodcastsInfiniteLazyRouteImport
parentRoute: typeof rootRouteImport
}
'/podcasts': {
id: '/podcasts'
path: '/podcasts'
Expand Down Expand Up @@ -339,6 +361,7 @@ const rootRouteChildren: RootRouteChildren = {
LoginLazyRoute: LoginLazyRoute,
PlaygroundLazyRoute: PlaygroundLazyRoute,
PodcastsLazyRoute: PodcastsLazyRoute,
PodcastsInfiniteLazyRoute: PodcastsInfiniteLazyRoute,
AccountInboxInboxIdRoute: AccountInboxInboxIdRoute,
FriendsAccountAddressRoute: FriendsAccountAddressRoute,
SpaceSpaceIdRoute: SpaceSpaceIdRouteWithChildren,
Expand Down
3 changes: 3 additions & 0 deletions apps/events/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export const Route = createRootRoute({
<Link className="text-xs" to="/podcasts">
Podcasts
</Link>
<Link className="text-xs" to="/podcasts-infinite">
Podcasts Infinite Query
</Link>
<Link className="text-xs" to="/playground">
Playground
</Link>
Expand Down
48 changes: 48 additions & 0 deletions apps/events/src/routes/podcasts-infinite.lazy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEntitiesPublicInfinite } from '@graphprotocol/hypergraph-react';
import { createLazyFileRoute } from '@tanstack/react-router';
import { useEffect } from 'react';
import { Podcast } from '@/schema';

export const Route = createLazyFileRoute('/podcasts-infinite')({
component: RouteComponent,
});

function RouteComponent() {
const space = 'e252f9e1-d3ad-4460-8bf1-54f93b02f220';

const { data, isLoading, isError, fetchNextPage } = useEntitiesPublicInfinite(Podcast, {
first: 2,
offset: 0,
space: space,
include: {
projects: {},
},
});

useEffect(() => {
setTimeout(() => {
fetchNextPage();
setTimeout(() => {
fetchNextPage();
}, 1000);
}, 1000);
}, [fetchNextPage]);
Comment on lines +23 to +29

Copilot AI Nov 14, 2025

Copy link

Choose a reason for hiding this comment

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

The useEffect hook with fetchNextPage in the dependency array will cause an infinite loop or unexpected behavior. The fetchNextPage function reference can change between renders, causing the effect to re-run repeatedly.

This implementation also lacks proper cleanup - if the component unmounts before the timeouts complete, they will still execute.

For demo purposes, consider adding a button to trigger pagination manually, or add proper guards and cleanup:

useEffect(() => {
  let cancelled = false;
  const timer1 = setTimeout(() => {
    if (!cancelled) fetchNextPage();
    const timer2 = setTimeout(() => {
      if (!cancelled) fetchNextPage();
    }, 1000);
  }, 1000);
  
  return () => {
    cancelled = true;
    clearTimeout(timer1);
  };
}, []); // Empty dependency array
Suggested change
setTimeout(() => {
fetchNextPage();
setTimeout(() => {
fetchNextPage();
}, 1000);
}, 1000);
}, [fetchNextPage]);
const timer1 = setTimeout(() => {
fetchNextPage();
const timer2 = setTimeout(() => {
fetchNextPage();
}, 1000);
}, 1000);
return () => {
clearTimeout(timer1);
};
}, []);

Copilot uses AI. Check for mistakes.
console.log({ data, isLoading, isError });

return (
<>
<h1>Podcasts</h1>
{isLoading && <div>Loading...</div>}
{isError && <div>Error</div>}
{data?.pages.map((page) => {
return page.data.map((podcast) => (
<div key={podcast.id}>
<h2>{podcast.name}</h2>
<hr />
</div>
));
})}
{/* <pre className="text-xs">{JSON.stringify(data, null, 2)}</pre> */}
</>
);
}
3 changes: 1 addition & 2 deletions apps/events/src/routes/podcasts.lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ function RouteComponent() {

const { data, isLoading, isError } = useEntities(Podcast, {
mode: 'public',
first: 6,
offset: 0,
first: 100,

Copilot AI Nov 14, 2025

Copy link

Choose a reason for hiding this comment

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

The offset parameter has been removed. If pagination was previously being used with this component, removing offset: 0 without adding corresponding pagination logic could be a breaking change. Consider whether this removal is intentional or if pagination should be maintained.

Copilot uses AI. Check for mistakes.
space: space,
include: {
projects: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Constants, Entity, Utils } from '@graphprotocol/hypergraph';
import { useInfiniteQuery as useInfiniteQueryTanstack } from '@tanstack/react-query';
import * as Option from 'effect/Option';
import type * as Schema from 'effect/Schema';
import * as SchemaAST from 'effect/SchemaAST';
import type { QueryPublicParams } from '../internal/types.js';
import { useHypergraphSpaceInternal } from '../internal/use-hypergraph-space-internal.js';

export const useEntitiesPublicInfinite = <S extends Schema.Schema.AnyNoContext>(
type: S,
params?: QueryPublicParams<S>,
) => {
const { enabled = true, filter, include, space: spaceFromParams, first = 2, offset = 0 } = params ?? {};
const { space: spaceFromContext } = useHypergraphSpaceInternal();
const space = spaceFromParams ?? spaceFromContext;

// constructing the relation type ids for the query
const relationTypeIds = Utils.getRelationTypeIds(type, include);

const typeIds = SchemaAST.getAnnotation<string[]>(Constants.TypeIdsSymbol)(type.ast as SchemaAST.TypeLiteral).pipe(
Option.getOrElse(() => []),
);

const result = useInfiniteQueryTanstack({
queryKey: [
'hypergraph-public-entities',
space,
typeIds,
relationTypeIds.level1,
relationTypeIds.level2,
filter,

Copilot AI Nov 14, 2025

Copy link

Choose a reason for hiding this comment

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

The queryKey is missing the first parameter, which affects query caching. If the same query is made with different first values, they will share the same cache key and return incorrect results.

The queryKey should include first to ensure proper cache isolation:

queryKey: [
  'hypergraph-public-entities',
  space,
  typeIds,
  relationTypeIds.level1,
  relationTypeIds.level2,
  filter,
  first, // Add this
  'infinite',
],
Suggested change
filter,
filter,
first,

Copilot uses AI. Check for mistakes.
'infinite',
],
queryFn: async ({ pageParam }) => {
return Entity.findManyPublic(type, { filter, include, space, first, offset: pageParam });
},
getNextPageParam: (_lastPage, pages) => {
return offset + pages.length * first;
Comment on lines +37 to +38

Copilot AI Nov 14, 2025

Copy link

Choose a reason for hiding this comment

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

The getNextPageParam implementation has a bug. It calculates the next offset as offset + pages.length * first, but this doesn't account for the initial offset correctly when paginating.

For example:

  • If offset = 0 and first = 2, after fetching page 1 (pages.length = 1), the next offset would be 0 + 1 * 2 = 2
  • But if offset = 10 and first = 2, after fetching page 1, the next offset would be 10 + 1 * 2 = 12, which skips records 10-11

The calculation should be based on the initial page param, not the initial offset. Consider: pageParam + first or track cumulative records fetched.

Copilot uses AI. Check for mistakes.
},
initialPageParam: offset,
enabled,
});

return result;
};
1 change: 1 addition & 0 deletions packages/hypergraph-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { HypergraphSpaceProvider } from './HypergraphSpaceContext.js';
export { useCreateEntity } from './hooks/use-create-entity.js';
export { useDeleteEntity } from './hooks/use-delete-entity.js';
export { useEntities } from './hooks/use-entities.js';
export { useEntitiesPublicInfinite } from './hooks/use-entities-public-infinite.js';
export { useEntity } from './hooks/use-entity.js';
export { useHardDeleteEntity } from './hooks/use-hard-delete-entity.js';
export { usePrivyAuthCreatePrivateSpace as _usePrivyAuthCreatePrivateSpace } from './hooks/use-privy-auth-create-private-space.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/hypergraph-react/src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Entity } from '@graphprotocol/hypergraph';
import type * as Schema from 'effect/Schema';

export type QueryPublicParams<S extends Schema.Schema.AnyNoContext> = {
enabled: boolean;
enabled?: boolean | undefined;
filter?: Entity.EntityFilter<Schema.Schema.Type<S>> | undefined;
// TODO: for multi-level nesting it should only allow the allowed properties instead of Record<string, Record<string, never>>
include?: { [K in keyof Schema.Schema.Type<S>]?: Record<string, Record<string, never>> } | undefined;
Expand Down
Loading