I used Firebase for user authentication. I set functions for register users. then I created a signin function. When the user signs user redirects to user profile edit mode. Now I want to retrieve data when signing in user his/her particular data. I need to get a firebase user id to pass the another component when sign in. I try to get firebase user id and pass id to another component.Then I try to pass id to the backend using get function. Then I think i can retrive data for paticular user
signin function
async onSubmit(e) {
e.preventDefault();
const email = this.state.todo_email;
const password = this.state.todo_password;
try {
const signInresponse = await firebaseAuth.signInWithEmailAndPassword(email, password);
history.push('/User/Directory');
} catch (e) {
console.error(e);
}
Edit
I may have misread your question 😬
Your signInresponse should return a UserCredential which has a user property. The user prop can then be used to grab a bunch of other props.
So, signInresponse.user.uid will get you the uid...
I personally like the Promise structure and I destructure the user out, because it feels more event-driven.
firebaseAuth.signInWithEmailAndPassword(email, password)
.then(({ user }) => {
// do stuff
setUID(user.uid);
setUserName(user.displayName);
})
.catch((error) => {
handleWarning(error);
});
As an aside, check out the react-firebase-hooks. It can make things a little cleaner.
I hope this better answers your question.
You could use React.Context as a hook and expose it to your app as need. The usage is exactly like the useState() hook.
The main Context hook can be in a file:
// UserContext.js
import { createContext } from "react";
const userContextProvider = createContext({
userContext: {
name: null,
uid: null,
},
setUserContext: () => {},
});
export default userContextProvider;
In your base-level App you import the hook above, and wrap your project:
// App.js
import userContextProvider from "./api/UserContext";
const [userContext, setUserContext] = useState({ name: null, uid: null });
const value = { userContext, setUserContext };
return (
<userContextProvider.Provider value={value}>
<Router>
<Route path={} component={} ...
// ...
And then in the components you want to set or read your uid or name or whatever value:
// RandomComponent.js
import React, { useState, useContext } from "react";
import userContextProvider from "./api/UserContext";
const { userContext, setUserContext } = useContext(userContextProvider);
Then you can use userContext as your getter and setUserContext as your setter just like useState()
Related
I'm building an app with Next.js 13 and Supabase for the backend, and I've been stuck on figuring out the best/proper way to go about creating a context/provider for the current logged in user.
The flow to retrieve the user from Supabase is this:
Sign in with an OAuth Provider.
Grab the user ID from the session from the supabase onAuthState Changed hook.
Fetch the full user object from the supabase DB with the user ID mentioned above.
I have a supabase listener in my layout that listens for the auth state changes, and works well for setting and refreshing current session.
My initial approach was to add the fetchUser call from within the onAuthState changed hook, however I was running into late update hydration errors.
Taken directly from the examples, this is how the app looks:
// layout.tsx
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
cookies,
});
const {
data: { session },
} = await supabase.auth.getSession();
return (
<html>
<head />
<body>
<NavMenu session={session} />
<SupabaseListener accessToken={session?.access_token} />
{children}
</body>
</html>
);
}
// supabase-listener.tsx
// taken directly from the supabase-auth-helpers library.
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import supabase from "../lib/supabase/supabase-browser";
export default function SupabaseListener({
accessToken,
}: {
accessToken?: string;
}) {
const router = useRouter();
useEffect(() => {
supabase.auth.onAuthStateChange(async (event, session) => {
if (session?.access_token !== accessToken) {
router.refresh();
}
});
}, [accessToken, router]);
return null;
}
I basically just need to wrap my root layout with a LoggedInUserProvider, make the fetch user call somewhere in the initial page load, and set the state of the current logged in user provider.
The other approaches I tried was making the fetch user call from the root layout, and having a LoggedInUserListener client component that takes the user as a property and simply sets the state if the profile exists. This was causing improper set state errors.
Thank you so much.
Check out this PR for a better example of how to structure the application and add a provider for sharing a single instance of Supabase client-side, as well as the session from the server 👍
If you follow a similar pattern, then your additional query for the full user record should go immediately after you get the session in examples/nextjs-server-components/app/layout.tsx. You could then pass this as a prop to the <SupabaseProvider /> and share it across the application from context's value prop.
I am following your awesome auth-helpers example but my context from the provider keeps coming back as null for user details. Is there anything wrong with the code below or is there some isLoading logic that will work better for getting that data?
Also want to confirm, does the SupabaseProvider in the root layout pass down to all other child layout components?
'use client';
import type { Session } from '#supabase/auth-helpers-nextjs';
import { createContext, useContext, useState, useEffect } from 'react';
import type { TypedSupabaseClient } from 'app/layout';
import { createBrowserClient } from 'utils/supabase-client';
import { UserDetails, CompanyDetails } from 'models/types';
type MaybeSession = Session | null;
type SupabaseContext = {
supabase: TypedSupabaseClient;
session: MaybeSession;
userDetails: UserDetails | null;
isLoading: boolean;
};
// #ts-ignore
const Context = createContext<SupabaseContext>();
//TODO get stripe subscription data
export default function SupabaseProvider({
children,
session
}: {
children: React.ReactNode;
session: MaybeSession;
}) {
const [supabase] = useState(() => createBrowserClient());
const [userDetails, setUserDetails] = useState<UserDetails | null>(null);
const [isLoading, setLoading] = useState(false);
// Hydrate user context and company data for a user
useEffect(() => {
const fetchUserDetails = async () => {
if (session && session.user) {
setLoading(true);
const { data } = await supabase
.from('users')
.select('*, organizations (*)')
.eq('id', session.user.id)
.single();
//TODO fix types
setUserDetails(data as any);
setLoading(false);
}
};
if (session) {
fetchUserDetails();
}
}, [session, supabase]);
return (
<Context.Provider value={{ supabase, session, userDetails, isLoading }}>
<>{children}</>
</Context.Provider>
);
}
export const useSupabase = () => useContext(Context);
I'm trying to implement authentication in my app using Firebase and I need to store some custom user fields (e.g. schoolName, programType, etc.) on the user documents that I'm storing in Firestore. I want to have these custom fields in my React state (I'm using Recoil for state management), and I'm very unsure of the best way to do this.
I currently have a Cloud Function responsible for creating a new user document when new auth users are created, which is great, however, I'm having trouble figuring out a good way to get that new user (with the custom fields) into my state, so I came up with a solution but I'm not sure if it's ideal and would love some feedback:
I define the firebase/auth functions (e.g. signInWithPopup, logout, etc.) in an external static file and simply import them in my login/signup forms.
To manage the user state, I created a custom hook useAuth:
const useAuth = () => {
const [user] = useAuthState(auth); // firebase auth state
const [currentUser, setCurrentUser] = useRecoilState(userState); // global recoil state
useEffect(() => {
// User has logged out; firebase auth state has been cleared; so clear app state
if (!user?.uid && currentUser) {
return setCurrentUser(null);
}
const userDoc = doc(firestore, "users", user?.uid as string);
const unsubscribe = onSnapshot(userDoc, (doc) => {
console.log("CURRENT DATA", doc.data());
if (!doc.data()) return;
setCurrentUser(doc.data() as any);
});
if (currentUser) {
console.log("WE ARE UNSUBBING FROM LISTENER");
unsubscribe();
}
return () => unsubscribe();
}, [user, currentUser]);
};
This hook uses react-firebase-hooks and attempts to handle all cases of the authentication process:
New users
Existing users
Persisting user login on refresh (the part that makes this most complicated - I think)
To summarize the above hook, it essentially listens to changes in firebase auth state via useAuthState, then I add a useEffect which creates a listener of the user document in firestore, and when that user has successfully been inputted into the db by the Cloud Function, the listener will fire, and it will populate recoil state with doc.data() (which contains the custom fields) via setCurrentUser. As for existing users, the document will already exist, so a single snapshot will do the trick. The rationale behind the listener is the case of new users, where a second snapshot will be required as the first doc.data() will be undefined even though useAuthState will have a user in it, so it's essentially just waiting for the Cloud Function to finish.
I call this hook immediately as the app renders to check for a Firebase Auth user in order to persist login on refresh/revisit.
I've been messing around on this for quite some time, and this outlined solution does work, but I have come up with multiple solutions so I would love some guidance.
Thank you very much for reading.
Step 1: Define CurrentUser, and UserProfile states
import { atom, selector } from "recoil";
import { type User } from "firebase/auth";
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
});
export const UserProfile = atomFamily<Profile | null, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return undefined;
}
});
Step 2: Listen to the authenticated user state changes
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
effects: [
(ctx) => {
if (ctx.trigger === "get") {
// Import Firebase App instanced defined in a separate chunk
const promise = import("../core/firebase")
.then((fb) =>
fb.auth.onAuthStateChanged((user) => {
ctx.setSelf(user);
})
)
.catch((err) => ctx.setSelf(Promise.reject(err)));
return () => promise.then((unsubscribe) => unsubscribe?.());
}
},
],
});
Step 3: Load user profile by Firebase user UID
export const UserProfile = atomFamily<User | null | undefined, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return async function() {
if (!uid) return null;
import("../core/firebase").then(({ fs }) => {
// TODO: Retrieve Firestore document with the user profile
return getDoc(doc(dollection(fs, "users"), uid));
});
};
}
});
Step 4: Add React hooks
import { useRecoilValue } from "recoil";
export function useCurrentUser() {
return useRecoilValue(CurrentUser);
}
export function useCurrentUserProfile() {
const me = useRecoilValue(CurrentUser);
return useRecoilValue(UserProfile(me?.uid));
}
Usage Example
import { useCurrentUser, useCurrentUserProfile } from "../state/firebase";
export function Example(): JSX.Element {
const me = useCurrentUser(); // Firebase user object
const profile = useCurrentUserProfile(); // Custom profile from Firestore
}
See https://github.com/kriasoft/cloudflare-starter-kit for a working example
I have a small dummy react application, I have created a "Home Page" after the user is logged on the home page context is set for user credentials from the home page and I am able to get it on the home page. but I have created another page "profile" after I change the route to profile in URL, there is no longer data. I understand that since the page is refreshed data is lost because data persistence is the job of databases. but if I have to query some common data for every page what advantage does context put on the table? thanks.
On Home Page below the code, I wrote to set user credentials.
Home.js
**const { userCredentails, setUserCredentails } = useUserContext();**
useEffect(() => {
const getInitialData = async () => {
const returnData = await AuthFunction();
if (returnData.success) {
**setUserCredentails(returnData.user);**
return dispatch({ type: true, userData: returnData.user });
}
return dispatch({ type: false });
};
getInitialData();
}, []);
Now if a want to access the same data on the profile I don't want to query the database how do get it from there.
**const cntx = useContext(userCredentialsContext);**
above code returns empty objects.
finally, this is the context.js page
import { useState, createContext, useContext } from "react";
export const userCredentialsContext = createContext({});
export const UserContextProvider = ({ children }) => {
const [userCredentails, setUserCredentails] = useState({});
return (
<userCredentialsContext.Provider
value={{ userCredentails, setUserCredentails }}
>
{children}
</userCredentialsContext.Provider>
);
};
export const useUserContext = () => {
const data = useContext(userCredentialsContext);
return data;
};
if you implement your routing by using an anchor tag() or window.location, then the whole page will refresh including your context, setting it back to default state, the right way to do it is by using the Link tag from react-router-dom, here is an example;
import { Link } from "react-router-dom";
<Link to="/home">Home</Link>
This is my /chat page this is called from the /login page. I pass this chat page some data with
// this is inside of the page /login
history.push({
pathname: '/chat',
state: {
email: email,
name: password,
},
})
The problem is when I access /chat from /login everything works, but when I access /chat only I get the error TypeError: Cannot destructure property 'email' of 'state' as it is undefined.
Is there an option to query if state is empty? And if so take null?
import React, { useState, useEffect } from "react";
import axios from 'axios';
import { useLocation } from "react-router-dom";
function Chat() {
const { state } = useLocation();
const { email } = state;
const [person, setPerson] = useState([]);
const test_test = { email }
useEffect(() => {
axios.get('localhost:8000/'.concat('FILLER_', email.toString()))
.then((res) => {
const datapersons = res.data;
setPerson( datapersons );
})
.catch(error => {
console.log(error)
})
}, []);
return (
<div>
<h1>Welcome {person.givenname}</h1>
</div>
);
}
export default Chat
Check whether state exists before accessing it:
const email = state == null ? null : state.email;
If you're using TypeScript you can use the ?. syntax:
const email = state?.email;
The issue is when you go to /chat by itself you are not passing the link data to the new route anymore. This means that when you try to get state from useLocation() it won't be there as you didn't pass the data from login.
A solution I would recommend would be to cache the data in the browser and if you don't have useLocation() state you can then get it from the cached data. When you login you can save the information you want to the local cache here is some more detailed information on how to accomplish that. Wherever you login I would use
window.localStorage.setItem('email', 'userEmail');
and then in your component
const { email } = state ? state : window.localStorage.getItem('email');
And then when you log out make sure to clear out local storage unless you want to keep the user logged in between sessions you could use localStorage to accomplish that.
I am trying to useState to update the variables form the session storage, then I want to send an api request to my server and get some data. It all works if I hardcode in the email and password into the api request. But when I wait for the program to get the data the api request is running before the asynchronous useState function is finishing. I tried using useEffect and waiting for when user changes so that the function wouldn't run before it gets the data but that throws me into an infinite loop.
This is part of a broader program that takes the login data and authenticates. After that step i am looking to be able to move to a new page and then load in some data from my server but thats where it is breaking.
import axios from 'axios';
import Navbar from './Navbar';
import { UserContext } from './UserContext';
import React, { useContext, useState, useEffect } from 'react'
function Watchlist() {
const [user , setUser] = useState();
const [watchlist, setWatchlist] = useState();
setUser({
name: sessionStorage.getItem('sessionName'),
email: sessionStorage.getItem('sessionEmail'),
passowrd: sessionStorage.getItem('sessionPassword'),
authenticated: sessionStorage.getItem('sessionAuthenticated')
})
useEffect(() => {
getWatchlist()
}, [user])
function getWatchlist() {
console.log(`axios.post ${user.email} ${user.password}`);
axios.post(`/api/users/watchlist/id/${user.email}/${user.password}`).then((res) => {
if (res) {
setWatchlist(res.data);
console.log(JSON.stringify(res.data));
}
}).catch((err) => {
console.log(err);
})
}
return (
<div>
<Navbar />
<div>{JSON.stringify(watchlist)}</div>
</div>
)
} export default Watchlist;
You should never call a state updater unconditionally. Every time state updates, the component will re-render, so updating state unconditionally will always produce an infinite loop.
Just default your state object with the object you want.
const [user , setUser] = useState({
name: sessionStorage.getItem('sessionName'),
email: sessionStorage.getItem('sessionEmail'),
passowrd: sessionStorage.getItem('sessionPassword'),
authenticated: sessionStorage.getItem('sessionAuthenticated')
});