Basic app architecture question for Auth/session state retrieval & storage

This is my first larger full-stack app so please bear with me 😄 I have a basic question regarding how to handle authentication state throughout the app and different components. So my understanding is, that to some extent you don't actually need global state management since you can rely on the hooks like useSession etc. to sync your frontend with whether a user is authenticated or not / get user data. But I have some parts of user data that is stored in another table, which I need to query with tRPC / Prisma separately (user profile data eg. full name, avatar pic url, etc). My idea was to create a global store using Zustand and set that data there once a session is created. From then on, retrieve it only from there, throughout the app and all components that need that data. Then have some invalidation logic to clear the store data when the sessions ends. My reasoning behind this approach using a global store for user data would be that this way there would be no need to fetch the same piece of data multiple times and passing it around as props would lead to some deep prop drilling as the app grows. Now my question is, is there a better, more best-practice way of doing this? Am I missing something crucial? Thanks a lot
17 Replies
riccardolardi
riccardolardiOP•2y ago
My first implementation looks like this:
// _app.tsx
import { useAuth } from "~/store";

const AuthStateGrabber = () => {
const session = useSession();
const profile =
api.profiles.getById.useQuery({ id: session?.user.id }).data || null;
const setAuthData = useAuth((state) => state.setAuthData);

useEffect(() => {
setAuthData(session, profile);
}, [session, profile, setAuthData]);

return null;
};

const MyApp: AppType<{
initialSession: Session;
}> = ({ Component, pageProps }) => {
const [supabaseClient] = useState(() => createBrowserSupabaseClient());

return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<AuthStateGrabber />
<LayoutDefault>
<Component {...pageProps} />
</LayoutDefault>
</SessionContextProvider>
);
};

export default api.withTRPC(MyApp);
// _app.tsx
import { useAuth } from "~/store";

const AuthStateGrabber = () => {
const session = useSession();
const profile =
api.profiles.getById.useQuery({ id: session?.user.id }).data || null;
const setAuthData = useAuth((state) => state.setAuthData);

useEffect(() => {
setAuthData(session, profile);
}, [session, profile, setAuthData]);

return null;
};

const MyApp: AppType<{
initialSession: Session;
}> = ({ Component, pageProps }) => {
const [supabaseClient] = useState(() => createBrowserSupabaseClient());

return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<AuthStateGrabber />
<LayoutDefault>
<Component {...pageProps} />
</LayoutDefault>
</SessionContextProvider>
);
};

export default api.withTRPC(MyApp);
The store:
export interface AuthStore {
session: Session | null;
profile: Profile | null;
setAuthData: (session: Session | null, profile: Profile | null) => void;
}

export const useAuthStore = create<AuthStore>()((set, get) => ({
session: null,
profile: null,
setAuthData: (session, profile) => set(() => ({ session, profile })),
}));
export interface AuthStore {
session: Session | null;
profile: Profile | null;
setAuthData: (session: Session | null, profile: Profile | null) => void;
}

export const useAuthStore = create<AuthStore>()((set, get) => ({
session: null,
profile: null,
setAuthData: (session, profile) => set(() => ({ session, profile })),
}));
barry
barry•2y ago
I'm pretty sure it's create((set, get) => ({})) Not create()((set) => ({})) But I would also say you don't need a zustand store to store the user?
barry
barry•2y ago
You have https://github.com/supabase/auth-helpers which has a useSession
GitHub
GitHub - supabase/auth-helpers: A collection of framework specific ...
A collection of framework specific Auth utilities for working with Supabase. - GitHub - supabase/auth-helpers: A collection of framework specific Auth utilities for working with Supabase.
riccardolardi
riccardolardiOP•2y ago
Sorry, I was abstracting away the TS code and did an error. Code is corrected now
barry
barry•2y ago
It's still not create()((set, get) => ({}))
riccardolardi
riccardolardiOP•2y ago
My point is I'm storing additional user data somewhere else, in a different table whose data is not exposed by any auth helper method https://github.com/pmndrs/zustand/blob/main/docs/guides/typescript.md#basic-usage
barry
barry•2y ago
Ah there's a different way for Typescript lol didn't know that Ohh wait You can't use hooks inside random functions
const AuthStateGrabber = () => {
const session = useSession();
const profile =
api.profiles.getById.useQuery({ id: session?.user.id }).data || null;

useAuth((state) => state.setProfile(profile));

return null;
};
const AuthStateGrabber = () => {
const session = useSession();
const profile =
api.profiles.getById.useQuery({ id: session?.user.id }).data || null;

useAuth((state) => state.setProfile(profile));

return null;
};
This is a no no Only hooks and components can use hooks If you just copy pasted the contents of that function inside of App is it working Wait you're returning null and it's a component bro what is going on in this code lol api.profiles.getById.useQuery({ id: session?.user.id }) What is this? You have a whole seperate database?
riccardolardi
riccardolardiOP•2y ago
it's a renderless component no. just a different table
barry
barry•2y ago
And if you don't do that and just put this inside of App.tsx
const session = useSession();
const profile =
api.profiles.getById.useQuery({ id: session?.user.id }).data || null;

useAuth((state) => state.setProfile(profile));
const session = useSession();
const profile =
api.profiles.getById.useQuery({ id: session?.user.id }).data || null;

useAuth((state) => state.setProfile(profile));
Would it be working
riccardolardi
riccardolardiOP•2y ago
no because it's outside of supabase <SessionContextProvider>. That's why I use the renderless <AuthStateGrabber> component
barry
barry•2y ago
Ah
pradeep
pradeep•2y ago
Can u please check my problem?
barry
barry•2y ago
const session = useSession();
const setProfile = useAuth((state) => state.setProfile);
const profile = api.profiles.getById.useQuery(
{ id: session?.user.id },
{
enabled: !!session?.user.id,
onSuccess: (profil) => {
setProfile(profil);
},
}
);
const session = useSession();
const setProfile = useAuth((state) => state.setProfile);
const profile = api.profiles.getById.useQuery(
{ id: session?.user.id },
{
enabled: !!session?.user.id,
onSuccess: (profil) => {
setProfile(profil);
},
}
);
Maybe that works? I already did, spend some time going through the codebase, I don't know what's going wrong, please don't go into other people's questions just to ask for help
riccardolardi
riccardolardiOP•2y ago
@barry I solved the infinite loop problem using useEffect see my updated original question. Now the question is just about the pattern really. Whether this is a common pattern and if there's a better practice of handling such case
barry
barry•2y ago
Well what I just showed you lol Should work, we've got a query that only runs if session.user.id exists When that query is successful it takes said response and sets the profile to it
pradeep
pradeep•2y ago
uk I already wasted 3-4 h on the problem I am waiting for other people reply
barry
barry•2y ago
A lot of people are from the US here, it's like, 4 AM at most there, and in most of western Europe it's barely 12:00 (morning).
Want results from more Discord servers?
Add your server