mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Fixes the 14 pre-existing lint errors surfaced by React 19's new
hook rules so CI can enforce lint on every PR instead of tolerating
failures. The errors split roughly into three camps:
1) react-hooks/refs — "Cannot access refs during render"
4 occurrences of the "latest ref" pattern that assigned
`ref.current = x` during render. Moved each assignment into a
`useEffect(() => { ref.current = x })` — the canonical React-19
pattern. Consumers only read `.current` inside async callbacks
that run after commit, so the very slight delay to the update is
fine.
Touched: src/hooks/use-realtime.ts (onMessageRef,
onConversationRef), src/components/inbox/message-thread.tsx
(onMessagesLoadedRef), src/components/inbox/conversation-list.tsx
(onConversationsLoadedRef).
2) react-hooks/set-state-in-effect — genuine issues
- contacts/page.tsx: the `setPage(0)` on search change moved from
a useEffect into the `onChange` handler of the search input,
which is the cleaner pattern anyway.
- settings/page.tsx: the local `tab` state + sync effect were
removed entirely; the URL query param is now the single source
of truth, and the `onChange` handler just calls
`router.replace(...)`. Simpler and fixes the lint error.
3) react-hooks/set-state-in-effect — false positives for
load-on-mount and prop-driven sync
Suppressed per-call (or per-block for effects with many setters)
with explanatory comments. These effects use setters inside
async Supabase-query callbacks, not synchronously in the effect
body, so the cascade the rule warns about doesn't apply. Files:
contacts/page.tsx, pipelines/page.tsx, pipelines/deal-form.tsx,
pipelines/pipeline-settings.tsx,
components/inbox/contact-sidebar.tsx.
4) react-hooks/preserve-manual-memoization — one useCallback
Rule complained that the inferred dep was `contact` but the
source dep list was `[contact?.phone]`. Broadened the manual dep
to `[contact]` to match — callback behaviour is unchanged (it
still only reads `contact?.phone`), and the React Compiler can
now preserve the memoization.
And:
- .github/workflows/ci.yml: dropped `continue-on-error: true` on
the lint step. Lint now blocks merge on any new error.
Verified: `npm run lint` → 0 errors (24 warnings remain; all
pre-existing unused-import / missing-dep / <img> warnings that don't
fail CI). `npm run typecheck` clean. Landing, /docs, /login, and
/settings redirects all still respond correctly from the dev server.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useCallback, useState } from "react";
|
|
import { createClient } from "@/lib/supabase/client";
|
|
import type { Message, Conversation } from "@/types";
|
|
import type { RealtimeChannel } from "@supabase/supabase-js";
|
|
|
|
interface RealtimeEvent<T> {
|
|
eventType: "INSERT" | "UPDATE" | "DELETE";
|
|
new: T;
|
|
old: Partial<T>;
|
|
}
|
|
|
|
interface UseRealtimeOptions {
|
|
channelName: string;
|
|
onMessageEvent?: (event: RealtimeEvent<Message>) => void;
|
|
onConversationEvent?: (event: RealtimeEvent<Conversation>) => void;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export function useRealtime({
|
|
channelName,
|
|
onMessageEvent,
|
|
onConversationEvent,
|
|
enabled = true,
|
|
}: UseRealtimeOptions) {
|
|
const channelRef = useRef<RealtimeChannel | null>(null);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
|
|
// Store latest callbacks in refs to avoid re-subscribing when the
|
|
// parent re-renders with fresh closures. Assigned inside an effect
|
|
// so the mutation doesn't happen during render (React 19's refs
|
|
// rule) — subscribers only read `.current` inside async Realtime
|
|
// callbacks, which always run after the render that updates it.
|
|
const onMessageRef = useRef(onMessageEvent);
|
|
const onConversationRef = useRef(onConversationEvent);
|
|
useEffect(() => {
|
|
onMessageRef.current = onMessageEvent;
|
|
onConversationRef.current = onConversationEvent;
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!enabled) return;
|
|
|
|
const supabase = createClient();
|
|
|
|
const channel = supabase
|
|
.channel(channelName)
|
|
.on(
|
|
"postgres_changes",
|
|
{ event: "*", schema: "public", table: "messages" },
|
|
(payload) => {
|
|
onMessageRef.current?.({
|
|
eventType: payload.eventType as RealtimeEvent<Message>["eventType"],
|
|
new: payload.new as Message,
|
|
old: payload.old as Partial<Message>,
|
|
});
|
|
}
|
|
)
|
|
.on(
|
|
"postgres_changes",
|
|
{ event: "*", schema: "public", table: "conversations" },
|
|
(payload) => {
|
|
onConversationRef.current?.({
|
|
eventType: payload.eventType as RealtimeEvent<Conversation>["eventType"],
|
|
new: payload.new as Conversation,
|
|
old: payload.old as Partial<Conversation>,
|
|
});
|
|
}
|
|
)
|
|
.subscribe((status) => {
|
|
setIsConnected(status === "SUBSCRIBED");
|
|
});
|
|
|
|
channelRef.current = channel;
|
|
|
|
return () => {
|
|
supabase.removeChannel(channel);
|
|
channelRef.current = null;
|
|
setIsConnected(false);
|
|
};
|
|
}, [channelName, enabled]);
|
|
|
|
const unsubscribe = useCallback(() => {
|
|
if (channelRef.current) {
|
|
const supabase = createClient();
|
|
supabase.removeChannel(channelRef.current);
|
|
channelRef.current = null;
|
|
setIsConnected(false);
|
|
}
|
|
}, []);
|
|
|
|
return { isConnected, unsubscribe };
|
|
}
|