Skip to content

feat(web): typed api client + headless hooks (dashboard foundation)#201

Open
merencia wants to merge 2 commits into
feat/web-apifrom
feat/web-dashboard-hooks
Open

feat(web): typed api client + headless hooks (dashboard foundation)#201
merencia wants to merge 2 commits into
feat/web-apifrom
feat/web-dashboard-hooks

Conversation

@merencia

@merencia merencia commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

stacked on #200. the data foundation for the react dashboard: a typed hono client and headless hooks (logic split from presentation, the pro anti-fork seam).

  • createApiClient (hc<ApiApp>, relative paths) + useApiClient via context, so the pro/façade inject their own client
  • useApiQuery primitive: loading/error, refetch, polling, stale-response race guard (no react-query)
  • domain hooks: jobs (list/one/meta/actions), queues (list/toggle), overview (counts/timeseries)
  • source only; the ./dashboard subpath + build come with the app

Comment on lines +25 to +67
/**
* Minimal data-fetching primitive: runs `fetcher` on mount, whenever `deps` change, and on
* `refetch()`. A monotonic request id guards against races so a slow, stale response can
* never overwrite a newer one. No caching or dedup, by design; the API surface is small.
*/
export function useApiQuery<T>(fetcher: () => Promise<T>, deps: unknown[], options?: UseApiQueryOptions): QueryState<T> {
const [state, setState] = useState<{ data?: T; error?: Error; isLoading: boolean }>({ isLoading: true });

// Keep the latest fetcher in a ref (updated in an effect, never during render) so the run
// callback can stay stable without capturing a stale closure.
const fetcherRef = useRef(fetcher);
useEffect(() => {
fetcherRef.current = fetcher;
});

// Monotonic id: only the most recent request may write state.
const requestId = useRef(0);

const run = useCallback(() => {
const id = ++requestId.current;
setState((prev) => ({ ...prev, isLoading: true, error: undefined }));
fetcherRef.current().then(
(data) => {
if (id === requestId.current) setState({ data, isLoading: false });
},
(error: unknown) => {
if (id === requestId.current) setState({ error: error as Error, isLoading: false });
},
);
}, []);

useEffect(() => {
run();
if (options?.refetchInterval) {
const timer = setInterval(run, options.refetchInterval);
return () => clearInterval(timer);
}
// deps are the caller's cache key; run/refetchInterval are stable enough to omit.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);

return { data: state.data, error: state.error, isLoading: state.isLoading, refetch: run };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nah, let's use Tanstack Query

merencia added 2 commits July 8, 2026 16:20
…ation)

the data layer the react dashboard and the pro consume: a typed hono client (hc<ApiApp>,
relative paths) plus headless hooks over it (logic separated from presentation, the pro's
anti-fork seam).

- createApiClient + ApiClientProvider/useApiClient: the client is injected via context, so
  the pro passes its superset client and the façade a base-path client.
- useApiQuery: minimal fetch primitive with loading/error, refetch, polling and a
  stale-response race guard. no react-query.
- domain hooks: useJobs/useJob/useJobsMeta/useJobActions, useQueues/useQueueActions,
  useOverview/useOverviewTimeseries.

source only for now, not built or exported; the ./dashboard subpath + build land with the
app. 20 hook tests in jsdom (tdd); vitest/eslint/tsconfig wired to treat src/dashboard as
browser code.
Replace the hand-rolled useApiQuery primitive with TanStack Query
(useQuery/useMutation) over the typed hono client, per the plan-mode
decision for the dashboard v2 data layer.

- query hooks keep the { data, error, isLoading, refetch } shape, so
  consumers do not change; query keys are structural (no JSON.stringify)
- action hooks keep { run, cancel, rerun } / { toggle } but now invalidate
  their queries on success, dropping the manual refetch the callers did
- refetchInterval option preserves the existing poll
- adds @tanstack/react-query; test harness wraps a QueryClientProvider

Addresses review on use-api-query.ts (Giovani: use TanStack Query).
@merencia merencia force-pushed the feat/web-dashboard-hooks branch from ce13b43 to 13d0393 Compare July 8, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants