refactor(home): let useProfileOnboarding own the six profile states

5.3: profile / profileDraft / profileNotice / avatarNotice / avatarSaving /
profileSaving move into use-profile-onboarding.ts; Home reads them back from the
hook's return. The hook call moves above useBirthTimeGuidedJourney so profile is
defined before the first reader.

Turning the hook stateful made the React compiler lint rules apply to it, which
surfaced five errors that only a hook-shaped file gets checked for:
- two ref params written through .current (dialogReturnTarget,
  birthTimeRevisionPending) now end in "Ref" as the rule's hint asks;
- two render-phase ref writes (guidedBirthTimeReadyRef /
  editDeclaredBirthTimeDetailsRef) are gone: the hook already returns
  completeGuidedBirthTime and editDeclaredBirthTimeDetails, and now that it runs
  before useBirthTimeGuidedJourney they are passed straight in. Both callbacks
  were only ever read from render-scope closures, so the call target is the same
  function on every frame as before.

setProfile / setProfileDraft are listed in the two effects that call them; they
are raw useState setters handed through the return, so identity never changes
and neither effect gains a re-run.

Zero behavior change, zero copy change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
Jesse_Chen
2026-09-16 02:02:22 +00:00
co-authored by Claude Opus 5
parent 99425fe61f
commit ae4573897a
2 changed files with 69 additions and 74 deletions
+41 -39
View File
@@ -242,17 +242,12 @@ const BillingPanel = dynamic(
export default function Home() {
const router = useRouter();
const [profile, setProfile] = useState<Profile>(emptyProfile);
const [profileDraft, setProfileDraft] = useState<Profile>(emptyProfile);
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
const [activeAccountDialog, setActiveAccountDialog] = useState<AccountDialog | null>(null);
const [chartLibrary, setChartLibrary] = useState<ChartLibraryRecord[]>([]);
const [activeChartId, setActiveChartId] = useState("self");
const [billingPane, setBillingPane] = useState<OpenAccountDialogOptions>({});
const [dailyStarlanguage, setDailyStarlanguage] = useState<DailyStarlanguageState>({ kind: "pending" });
const [profileNotice, setProfileNotice] = useState("");
const [avatarNotice, setAvatarNotice] = useState("");
const [avatarSaving, setAvatarSaving] = useState(false);
const [account, setAccount] = useState<Account | null>(null);
const [accountError, setAccountError] = useState("");
const [signingOut, setSigningOut] = useState(false);
@@ -299,7 +294,6 @@ export default function Home() {
const [bootstrapPhase, setBootstrapPhase] = useState<BootstrapPhase>("account");
const prepareStartedAt = useRef<number | null>(null);
const [guidedJourneyPreview, setGuidedJourneyPreview] = useState(false);
const [profileSaving, setProfileSaving] = useState(false);
const [creatingSession, setCreatingSession] = useState(false);
const [sessionDetailLoadingId, setSessionDetailLoadingId] = useState<string | null>(null);
const [sessionFullPrompt, setSessionFullPrompt] = useState<{ question: string; theme: Theme } | null>(null);
@@ -315,7 +309,7 @@ export default function Home() {
const conversation = useRef<HTMLDivElement>(null);
const accountTrigger = useRef<HTMLButtonElement>(null);
const accountDialog = useRef<HTMLElement>(null);
const dialogReturnTarget = useRef<HTMLButtonElement | null>(null);
const dialogReturnTargetRef = useRef<HTMLButtonElement | null>(null);
const closeButton = useRef<HTMLButtonElement>(null);
const onboardingPaywallShown = useRef(false);
const starterGreetingUnseen = useRef(true);
@@ -348,17 +342,48 @@ export default function Home() {
const sessionDetailInFlight = useRef(new Set<string>());
const sessionsRef = useRef(sessions);
const rectificationSessionOpenerRef = useRef(async (_exactSessionId: string) => {});
const guidedBirthTimeReadyRef = useRef((_result: JourneyClientResponse) => {});
const editDeclaredBirthTimeDetailsRef = useRef(() => {});
const uiPreview = useRef(false);
const uiPreviewMode = useRef<string | null>(null);
const birthTimeRevisionPending = useRef(false);
const birthTimeRevisionPendingRef = useRef(false);
const {
profile,
setProfile,
profileDraft,
setProfileDraft,
profileNotice,
setProfileNotice,
avatarNotice,
avatarSaving,
profileSaving,
refreshAccount,
completeGuidedBirthTime,
editDeclaredBirthTimeDetails,
openAccountDialog,
closeAccountDialog,
persistAvatar,
saveProfile,
saveOnboardingName,
saveOnboardingBirth,
saveOnboardingPlace,
retryBirthTimeAssessment,
signOut,
} = useProfileOnboarding({
account, accountRefreshGuard, accountTrigger, activeChartId,
birthTimeRevisionPendingRef, chartLibrary, dialogReturnTargetRef,
setAccount, setAccountError, setAccountMenuOpen, setActiveAccountDialog,
setActiveChartId, setBirthTimeAssessmentPhase,
setBirthTimeConsultationConsent, setBirthTimeError, setBirthTimeJourney, setBillingPane, setDraft,
setOnboardingJustCompleted, setOnboardingStep, setPresetMessageLength,
setRectificationError,
setSigningOut, setStartGreeting, signingOut, uiPreview,
});
const birthTimeGuided = useBirthTimeGuidedJourney({
journey: birthTimeJourney,
preview: guidedJourneyPreview,
onJourney: setBirthTimeJourney,
onReady: (result) => guidedBirthTimeReadyRef.current(result),
onEditBirthTimeDetails: () => editDeclaredBirthTimeDetailsRef.current(),
onReady: (result) => completeGuidedBirthTime(result),
onEditBirthTimeDetails: () => editDeclaredBirthTimeDetails(),
});
const activeSession = sessions.find((session) => session.id === activeSessionId) ?? sessions[0];
@@ -437,29 +462,6 @@ export default function Home() {
openRectificationSession: (exactSessionId) => rectificationSessionOpenerRef.current(exactSessionId),
});
const {
refreshAccount,
openAccountDialog,
closeAccountDialog,
persistAvatar,
saveProfile,
saveOnboardingName,
saveOnboardingBirth,
saveOnboardingPlace,
retryBirthTimeAssessment,
signOut,
} = useProfileOnboarding({
account, accountRefreshGuard, accountTrigger, activeChartId, avatarSaving,
birthTimeRevisionPending, chartLibrary, dialogReturnTarget, profile, profileDraft,
profileSaving, setAccount, setAccountError, setAccountMenuOpen, setActiveAccountDialog,
setActiveChartId, setAvatarNotice, setAvatarSaving, setBirthTimeAssessmentPhase,
setBirthTimeConsultationConsent, setBirthTimeError, setBirthTimeJourney, setBillingPane, setDraft,
setOnboardingJustCompleted, setOnboardingStep, setPresetMessageLength,
setProfile, setProfileDraft, setProfileNotice, setProfileSaving, setRectificationError,
setSigningOut, setStartGreeting, signingOut, uiPreview,
guidedBirthTimeReadyRef, editDeclaredBirthTimeDetailsRef,
});
const { synastryPanel, clearSynastryHistory, applyCloudSynastryHistory } = useSynastry({
accountId, profile, chooseSuggestedQuestion, closeAccountDialog,
});
@@ -632,7 +634,7 @@ export default function Home() {
setProfile((current) => preserveShallowEqual(current, nextProfile));
if (syncSelfDraft) setProfileDraft((current) => preserveShallowEqual(current, nextProfile));
});
}, [accountId, activeChartId, chartLibrary, hydrated]);
}, [accountId, activeChartId, chartLibrary, hydrated, setProfile, setProfileDraft]);
const profileComplete = isProfileComplete(profile);
const birthTimeRoute = resolveBirthTimeConsultationRoute(profile, birthTimeConsultationConsent, activeSessionId);
@@ -1096,7 +1098,7 @@ export default function Home() {
window.clearTimeout(bootstrapTimeout);
controller.abort();
};
}, []);
}, [setProfile, setProfileDraft]);
useEffect(() => {
if (!hydrated || uiPreview.current) return;
@@ -1313,7 +1315,7 @@ export default function Home() {
if (event.key === "Escape") {
if (signingOut) return;
setActiveAccountDialog(null);
const returnTarget = dialogReturnTarget.current;
const returnTarget = dialogReturnTargetRef.current;
window.requestAnimationFrame(() => returnTarget?.focus());
return;
}
@@ -1514,7 +1516,7 @@ export default function Home() {
dialogClass: activeAccountDialog ? accountDialogClasses[activeAccountDialog] : "",
signingOut,
close: closeAccountDialog,
navigate: (dialog) => openAccountDialog(dialog, dialogReturnTarget.current),
navigate: (dialog) => openAccountDialog(dialog, dialogReturnTargetRef.current),
overlayRef: accountDialog,
closeButtonRef: closeButton,
renderProfile: () => (
+28 -35
View File
@@ -1,6 +1,6 @@
"use client";
import { type Dispatch, FormEvent, MutableRefObject, SetStateAction } from "react";
import { useState, type Dispatch, FormEvent, MutableRefObject, SetStateAction } from "react";
import {
beamAvatarSchema,
@@ -43,6 +43,7 @@ import {
type OpenAccountDialogOptions,
type Profile,
accountDialogOptions,
emptyProfile,
} from "@/lib/home-types";
import { preserveShallowEqual } from "@/lib/preserve-shallow-equal";
import {
@@ -58,20 +59,14 @@ export type ProfileOnboardingParams = {
accountRefreshGuard: MutableRefObject<LatestAccountRequestGuard>;
accountTrigger: MutableRefObject<HTMLButtonElement | null>;
activeChartId: string;
avatarSaving: boolean;
birthTimeRevisionPending: MutableRefObject<boolean>;
birthTimeRevisionPendingRef: MutableRefObject<boolean>;
chartLibrary: ChartLibraryRecord[];
dialogReturnTarget: MutableRefObject<HTMLButtonElement | null>;
profile: Profile;
profileDraft: Profile;
profileSaving: boolean;
dialogReturnTargetRef: MutableRefObject<HTMLButtonElement | null>;
setAccount: Dispatch<SetStateAction<Account | null>>;
setAccountError: Dispatch<SetStateAction<string>>;
setAccountMenuOpen: Dispatch<SetStateAction<boolean>>;
setActiveAccountDialog: Dispatch<SetStateAction<AccountDialog | null>>;
setActiveChartId: Dispatch<SetStateAction<string>>;
setAvatarNotice: Dispatch<SetStateAction<string>>;
setAvatarSaving: Dispatch<SetStateAction<boolean>>;
setBirthTimeAssessmentPhase: Dispatch<SetStateAction<BirthTimeAssessmentPhase | null>>;
setBirthTimeConsultationConsent: Dispatch<SetStateAction<BirthTimeConsultationConsentState>>;
setBirthTimeError: Dispatch<SetStateAction<string>>;
@@ -81,17 +76,11 @@ export type ProfileOnboardingParams = {
setOnboardingJustCompleted: Dispatch<SetStateAction<boolean>>;
setOnboardingStep: Dispatch<SetStateAction<OnboardingStep>>;
setPresetMessageLength: Dispatch<SetStateAction<number>>;
setProfile: Dispatch<SetStateAction<Profile>>;
setProfileDraft: Dispatch<SetStateAction<Profile>>;
setProfileNotice: Dispatch<SetStateAction<string>>;
setProfileSaving: Dispatch<SetStateAction<boolean>>;
setRectificationError: Dispatch<SetStateAction<string>>;
setSigningOut: Dispatch<SetStateAction<boolean>>;
setStartGreeting: Dispatch<SetStateAction<string>>;
signingOut: boolean;
uiPreview: MutableRefObject<boolean>;
guidedBirthTimeReadyRef: MutableRefObject<(result: JourneyClientResponse) => void>;
editDeclaredBirthTimeDetailsRef: MutableRefObject<() => void>;
};
export function useProfileOnboarding(params: ProfileOnboardingParams) {
@@ -100,20 +89,14 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
accountRefreshGuard,
accountTrigger,
activeChartId,
avatarSaving,
birthTimeRevisionPending,
birthTimeRevisionPendingRef,
chartLibrary,
dialogReturnTarget,
profile,
profileDraft,
profileSaving,
dialogReturnTargetRef,
setAccount,
setAccountError,
setAccountMenuOpen,
setActiveAccountDialog,
setActiveChartId,
setAvatarNotice,
setAvatarSaving,
setBirthTimeAssessmentPhase,
setBirthTimeConsultationConsent,
setBirthTimeError,
@@ -123,19 +106,22 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
setOnboardingJustCompleted,
setOnboardingStep,
setPresetMessageLength,
setProfile,
setProfileDraft,
setProfileNotice,
setProfileSaving,
setRectificationError,
setSigningOut,
setStartGreeting,
signingOut,
uiPreview,
guidedBirthTimeReadyRef,
editDeclaredBirthTimeDetailsRef,
} = params;
// Owned here since 2026-09-16 (state lowering batch 2). Home reads them back
// from this hook's return instead of declaring them itself.
const [profile, setProfile] = useState<Profile>(emptyProfile);
const [profileDraft, setProfileDraft] = useState<Profile>(emptyProfile);
const [profileNotice, setProfileNotice] = useState("");
const [avatarNotice, setAvatarNotice] = useState("");
const [avatarSaving, setAvatarSaving] = useState(false);
const [profileSaving, setProfileSaving] = useState(false);
async function refreshAccount() {
const requestIdentity = accountRefreshGuard.current.begin();
try {
@@ -162,7 +148,7 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
returnTargetOrOptions?: HTMLButtonElement | null | OpenAccountDialogOptions,
) {
const options = accountDialogOptions(returnTargetOrOptions);
dialogReturnTarget.current = options.returnTarget ?? accountTrigger.current;
dialogReturnTargetRef.current = options.returnTarget ?? accountTrigger.current;
setAccountMenuOpen(false);
setAccountError("");
if (dialog === "profile") {
@@ -186,7 +172,7 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
function closeAccountDialog() {
if (signingOut) return;
setActiveAccountDialog(null);
const returnTarget = dialogReturnTarget.current;
const returnTarget = dialogReturnTargetRef.current;
window.requestAnimationFrame(() => returnTarget?.focus());
}
@@ -337,7 +323,7 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
setAccountError("");
try {
const savedProfile = await persistProfile(profileDraft);
birthTimeRevisionPending.current = false;
birthTimeRevisionPendingRef.current = false;
setProfile(savedProfile);
setProfileDraft(savedProfile);
setPresetMessageLength(0);
@@ -353,7 +339,7 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
}
function editDeclaredBirthTimeDetails() {
birthTimeRevisionPending.current = true;
birthTimeRevisionPendingRef.current = true;
setBirthTimeError("");
setPresetMessageLength(0);
setOnboardingStep("birth");
@@ -423,10 +409,17 @@ export function useProfileOnboarding(params: ProfileOnboardingParams) {
}
}
guidedBirthTimeReadyRef.current = completeGuidedBirthTime;
editDeclaredBirthTimeDetailsRef.current = editDeclaredBirthTimeDetails;
return {
profile,
setProfile,
profileDraft,
setProfileDraft,
profileNotice,
setProfileNotice,
avatarNotice,
avatarSaving,
profileSaving,
refreshAccount,
openAccountDialog,
closeAccountDialog,