Skip to content

Commit 16385ed

Browse files
committed
Derive auth state idiomatically in AuthProvider
Replace the seven hand-tracked primitive fields with a memoized resolved state: useMemo on the atom value gives the identify and hint-cookie effects a dependency that tracks real query transitions, and the context value is memoized so consumers don't re-render on unrelated renders.
1 parent af37833 commit 16385ed

1 file changed

Lines changed: 40 additions & 54 deletions

File tree

packages/react/src/multiplayer/auth-context.tsx

Lines changed: 40 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { createContext, useContext, useEffect } from "react";
1+
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
22
import { useAtomValue } from "@effect/atom-react";
33
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
44

@@ -68,72 +68,58 @@ const AuthProviderClient = ({
6868
onIdentify?: IdentifyFn;
6969
}) => {
7070
const result = useAtomValue(meAtom);
71+
7172
// The hint applies one frame AFTER mount: SSR always renders the loading
72-
// state, and React requires the first client render to match that HTML, so
73-
// reading the cookie during render would be a hydration mismatch. The
74-
// post-mount flip costs a frame, not a network round trip — and the hint
75-
// only seeds the in-flight window; `/account/me` is the authority from its
76-
// first resolution on.
77-
const [hint, setHint] = React.useState<AuthHint | null>(null);
73+
// state and the first client render must match that HTML, so the cookie
74+
// read is deferred to an effect (the documented pattern for client-only
75+
// values). The flip costs a frame, not a network round trip.
76+
const [hint, setHint] = useState<AuthHint | null>(null);
7877
useEffect(() => {
7978
setHint(readAuthHintCookie());
8079
}, []);
8180

82-
// What `/account/me` actually said — the authority.
83-
const resolved: AuthState = AsyncResult.match(result, {
84-
onInitial: () => ({ status: "loading" as const }),
85-
onSuccess: ({ value }) => ({
86-
status: "authenticated" as const,
87-
user: value.user,
88-
organization: value.organization,
89-
}),
90-
onFailure: () => ({ status: "unauthenticated" as const }),
91-
});
92-
93-
// What consumers see — the hint papers over the in-flight window only.
94-
const state: AuthState = resolved.status === "loading" ? (hintState(hint) ?? resolved) : resolved;
95-
96-
// Primitive identity fields of the RESOLVED state, so the effects below
97-
// fire only on real transitions (the objects are rebuilt every render) and
98-
// never on hint-derived optimistic state — identify must not report a stale
99-
// hint to analytics, and the hint cookie must not rewrite itself.
100-
const status = resolved.status;
101-
const userId = resolved.status === "authenticated" ? resolved.user.id : null;
102-
const email = resolved.status === "authenticated" ? resolved.user.email : null;
103-
const name = resolved.status === "authenticated" ? resolved.user.name : null;
104-
const avatarUrl = resolved.status === "authenticated" ? resolved.user.avatarUrl : null;
105-
const organizationId =
106-
resolved.status === "authenticated" ? (resolved.organization?.id ?? null) : null;
107-
const organizationName =
108-
resolved.status === "authenticated" ? (resolved.organization?.name ?? null) : null;
109-
81+
// What `/account/me` actually said — the authority. The atom value only
82+
// changes identity when the query emits, so memoizing on it gives the
83+
// effects below a dependency that tracks real transitions.
84+
const resolved = useMemo<AuthState>(
85+
() =>
86+
AsyncResult.match(result, {
87+
onInitial: () => ({ status: "loading" as const }),
88+
onSuccess: ({ value }) => ({
89+
status: "authenticated" as const,
90+
user: value.user,
91+
organization: value.organization,
92+
}),
93+
onFailure: () => ({ status: "unauthenticated" as const }),
94+
}),
95+
[result],
96+
);
97+
98+
// Both effects key off RESOLVED state, never hint-derived optimism:
99+
// identify must not report a stale hint to analytics, and the hint cookie
100+
// must not rewrite itself from its own contents.
110101
useEffect(() => {
111-
if (!onIdentify) return;
112-
if (status === "authenticated" && userId && email !== null) {
113-
onIdentify({
114-
status: "authenticated",
115-
user: { id: userId, email, name, avatarUrl },
116-
organization: organizationId ? { id: organizationId, name: organizationName ?? "" } : null,
117-
});
118-
} else if (status === "unauthenticated") {
119-
onIdentify({ status: "unauthenticated" });
120-
}
121-
}, [onIdentify, status, userId, email, name, avatarUrl, organizationId, organizationName]);
102+
if (resolved.status === "loading") return;
103+
onIdentify?.(resolved);
104+
}, [onIdentify, resolved]);
122105

123106
// Keep the hint cookie in step with reality so the NEXT page load seeds
124107
// correctly: refresh it on every confirmed identity, drop it the moment the
125108
// server says signed-out.
126109
useEffect(() => {
127-
if (status === "authenticated" && userId && email !== null) {
128-
writeAuthHintCookie({
129-
v: 1,
130-
user: { id: userId, email, name, avatarUrl },
131-
organization: organizationId ? { id: organizationId, name: organizationName ?? "" } : null,
132-
});
133-
} else if (status === "unauthenticated") {
110+
if (resolved.status === "authenticated") {
111+
writeAuthHintCookie({ v: 1, user: resolved.user, organization: resolved.organization });
112+
} else if (resolved.status === "unauthenticated") {
134113
clearAuthHintCookie();
135114
}
136-
}, [status, userId, email, name, avatarUrl, organizationId, organizationName]);
115+
}, [resolved]);
116+
117+
// What consumers see — the hint papers over the in-flight window only.
118+
// Memoized so context consumers don't re-render on unrelated renders.
119+
const state = useMemo<AuthState>(
120+
() => (resolved.status === "loading" ? (hintState(hint) ?? resolved) : resolved),
121+
[resolved, hint],
122+
);
137123

138124
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
139125
};

0 commit comments

Comments
 (0)