Skip to main content

useCircleFeedState

Location: src/hooks/useCircleFeedState.js

Overview

Provides hooks for circle-specific feed pacing. Each circle maintains its own "caught up" state so users can see which circles have new content. Includes useCircleFeedState for a single circle's state, useCircleNewPostCount for new post counts, useMarkCircleChecked for marking a circle as read, and useAllCirclesNewPostCounts for aggregated counts across all circles.

Refreshes use visibilitychange events (when the user returns to the tab) instead of polling, in line with Invariant #12 (Slowness Is Baked In).

Exported Hooks

useCircleFeedState

Gets the feed state for a specific circle.

function useCircleFeedState(circleId: string): {
loading: boolean,
error: string | null,
feedState: object | null,
lastCheckedAt: Date,
refetch: () => Promise<void>
}
ParameterTypeRequiredDescription
circleIdstringYesThe circle ID
PropertyTypeDescription
feedStateobject | nullRaw feed state row from circle_feed_state table
lastCheckedAtDateWhen the user last viewed this circle's feed. Defaults to epoch if never checked.

useCircleNewPostCount

Gets the number of new posts in a specific circle since last checked.

function useCircleNewPostCount(circleId: string): {
loading: boolean,
error: string | null,
newCount: number,
refetch: () => Promise<void>
}
PropertyTypeDescription
newCountnumberNumber of new posts since last checked

useMarkCircleChecked

Marks a circle as checked (caught up).

function useMarkCircleChecked(): {
markChecked: (circleId: string) => Promise<{ success?: boolean, error?: string }>,
loading: boolean,
error: string | null
}
PropertyTypeDescription
markChecked(circleId) => Promise<Result>Mark a circle as checked via mark_circle_checked RPC

useAllCirclesNewPostCounts

Gets new post counts for all of the user's circles at once.

function useAllCirclesNewPostCounts(): {
loading: boolean,
error: string | null,
counts: Array<{ circle_id: string, new_count: number }>,
getNewCount: (circleId: string) => number,
getTotalNewCount: () => number,
refetch: () => Promise<void>
}
PropertyTypeDescription
countsArrayArray of { circle_id, new_count } objects
getNewCount(circleId) => numberGet the new post count for a specific circle
getTotalNewCount() => numberGet total new posts across all circles

Usage

import {
useCircleNewPostCount,
useMarkCircleChecked,
useAllCirclesNewPostCounts
} from '../hooks/useCircleFeedState';

function CircleNav({ circles }) {
const { getNewCount, getTotalNewCount } = useAllCirclesNewPostCounts();

return (
<nav>
<span>Total new: {getTotalNewCount()}</span>
{circles.map(c => (
<a key={c.id} href={`/circles/${c.id}`}>
{c.name} {getNewCount(c.id) > 0 && `(${getNewCount(c.id)} new)`}
</a>
))}
</nav>
);
}

function CircleFeed({ circleId }) {
const { newCount } = useCircleNewPostCount(circleId);
const { markChecked } = useMarkCircleChecked();

useEffect(() => {
// Mark as checked when viewing the feed
markChecked(circleId);
}, [circleId]);

return <Feed circleId={circleId} />;
}

Notes

  • No polling is used. useCircleNewPostCount and useAllCirclesNewPostCounts refresh when the browser tab becomes visible via document.visibilitychange.
  • lastCheckedAt defaults to epoch (new Date(0)) if the user has never viewed a circle, so all posts will count as new.
  • New post counts are fetched via the get_circle_new_post_count and get_all_circles_new_post_counts RPC functions.
  • The current user is fetched internally by each hook via supabase.auth.getUser().

Last updated: 2026-02-07