How to call Auth.currentAuthenticatedUser() after login is made in another component? - reactjs

I have AppBar component which I want to show currently logged in user. On the same page I have Profile component, which triggers login form. The problem is that they are not synched, obviously currentAuthenticatedUser inside AppBar is called before login is made and not called after, unless page is refreshed:
function AppBar() {
const [user, setUser] = useState();
useEffect(() => {
Auth.currentAuthenticatedUser()
.then((u) => {
setUser(u.username);
});
}, []);
return <div>AppBar: {user}</div>
}
function Profile(props: any) {
return <Authenticator>
{({ user, signOut }: any) => {
return <>
<div>Profile: { user.username }</div>
<button onClick={() => signOut()}>Sign Out</button>
</>
}}
</Authenticator>
}
function App() {
return <b><AppBar /><Profile /></b>
}
After login:
After page refresh:
How to display username in AppBar right after login without the need to refresh the page? Thanks!

You can achieve that functionality by either using local state or context
Local state:
setup your const [user, setUser] = useState(); hook on top of AppBar and Profile components. Then pass down user as prop on the AppBar component and pass down setUser on the Profile component, you would need to create an internal component inside Profile that would take this prop and then be rendered inside the Authenticator component.
Context:
It is a similar process as above but you need to use the React.createContext(<<context_data_here>>) syntax. I would suggest using this approach if your app need access to user on many components. If that is not the case using local state is recommended.
If you are not familiar with context I'd suggest watching this video

Solution borrowed from here
function AppBar() {
const [signedUser, setSignedUser] = useState<{username: string}>();
useEffect(() => {
authListener();
}, []);
async function authListener() {
Hub.listen("auth", (data) => {
switch (data.payload.event) {
case "signIn":
return setSignedUser(data.payload.data);
case "signOut":
return setSignedUser(undefined);
}
});
try {
const user = await Auth.currentAuthenticatedUser();
setSignedUser(user);
} catch (err) {}
}
return <div>AppBar: {signedUser?.username}</div>
}

Related

Nexts.js 13 + Supabase > What's the proper way to create a user context

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);

How to controling browser back button with react router dom v6?

I've been looking for this question and found it but they're using class components and react router dom v5
What i want is
When user click browser back button I'll redirect them to home page
If you are simply wanting to run a function when a back navigation (POP action) occurs then a possible solution is to create a custom hook for it using the exported NavigationContext.
Example:
import { UNSAFE_NavigationContext } from "react-router-dom";
const useBackListener = (callback) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
const listener = ({ location, action }) => {
console.log("listener", { location, action });
if (action === "POP") {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Usage:
import { useNavigate } from 'react-router-dom';
import { useBackListener } from '../path/to/useBackListener';
...
const navigate = useNavigate();
useBackListener(({ location }) =>
console.log("Navigated Back", { location });
navigate("/", { replace: true });
);
If using the UNSAFE_NavigationContext context is something you'd prefer to avoid then the alternative is to create a custom route that can use a custom history object (i.e. from createBrowserHistory) and use the normal history.listen. See my answer here for details.
Update w/ Typescript
import { useEffect, useContext } from "react";
import { NavigationType, UNSAFE_NavigationContext } from "react-router-dom";
import { History, Update } from "history";
const useBackListener = (callback: (...args: any) => void) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator as History;
useEffect(() => {
const listener = ({ location, action }: Update) => {
console.log("listener", { location, action });
if (action === NavigationType.Pop) {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Well after a long journey to find out how to do that finally i came up with this solution
window.onpopstate = () => {
navigate("/");
}
I came up with a pretty robust solution for this situation, just using browser methods, since react-router-v6's API is pretty sketchy in this department right now.
I push on some fake history identical to the current route (aka a buffer against the back button). Then, I listen for a popstate event (back button event) and fire whatever JS I need, which in my case unmounts the component. If the component unmounts WITHOUT the use of the back button, like by an onscreen button or other logic, we just clean up our fake history using useEffect's callback. Phew. So it looks like:
function closeQuickView() {
closeMe() // do whatever you need to close this component
}
useEffect(() => {
// Add a fake history event so that the back button does nothing if pressed once
window.history.pushState('fake-route', document.title, window.location.href);
addEventListener('popstate', closeQuickView);
// Here is the cleanup when this component unmounts
return () => {
removeEventListener('popstate', closeQuickView);
// If we left without using the back button, aka by using a button on the page, we need to clear out that fake history event
if (window.history.state === 'fake-route') {
window.history.back();
}
};
}, []);
You can go back by using useNavigate hook, that has become with rrd v6
import {useNabigate} from "react-router-dom";
const App = () => {
const navigate = useNavigate();
const goBack = () => navigate(-1);
return (
<div>
...
<button onClick={goBack}>Go back</button>
...
</div>
)
}
export App;
I used <Link to={-1}>go back</Link> and its working in v6, not sure if it's a bug or a feature but seems there is no error in console and can't find any documentation stating this kind of approach
You can try this approach. This worked for me.
import { useNavigate, UNSAFE_NavigationContext } from "react-router-dom";
const navigation = useContext(UNSAFE_NavigationContext).navigator;
const navigate = useNaviagte();
React.useEffect(() => {
let unlisten = navigation.listen((locationListener) => {
if (locationListener.action === "POP") {
//do your stuff on back button click
navigate("/");
}
});
return(() => {
unlisten();
})
}, []);
I'm on rrd#6.8 and testing John's answer worked for me right away for a simple "GO back 1 page", no useNavigate needed:
<Link to={-1}>
<Button size="sm">← Back </Button>
</Link>
So as a simple back button this seems to work without unexpected errors.

Rendering another view in Parent after Child component callback

I really don't know how I can do this, I have a login.tsx file and App.tsx file:
in login.tsx I have:
const Login = () => {
const classes = useStyles();
const [state, dispatch] = useReducer(reducer, initialState);
[...]
const handleLogin = () => {
if (state.password === 'password' && state.username.length > 2) {
dispatch({
type: 'loginSuccess',
payload: 'Login Successfully'
});
// here I want to add the callback to the parent function App
} else {
dispatch({
type: 'loginFailed',
payload: 'Username too short or incorrect password'
});
}
};
and in App.tsx:
if (true) { // replace true with the callback from the child
return <Login />
}
return ( // (else)
<div className="App"> // the other view we want once logged in successfully
[...]
So that once the user is logged in it automatically render another view. Sorry for the very stupid question I am not very experienced with React.
NB: I know this is not the secured way to do it (login auth should be with JWT/OAuth etc).
You will need a state in your App.tsx that is responsible for keeping track of which component to render, e.g.: const [showLogin, setShowLogin] = useState(true);.
Then use the showLogin variable in your if/else statement.
You will also need to pass down a function as a prop to your Login component that will update showLogin once the logging in has completed, like so:
<Login onLogin={() => setShowLogin(false)} />.
Then, in your handleLogin function, call the onLogin function passed down to your Login component.
edit from OP:
Also slightly changed the code to const Login = (props: {onLogin: any }) => {
and call props.onLogin();

How do I call props.history.push() if I'm destructuring my props?

If I've got a function that creates a confirm popup when you click the back button, I want to save the state before navigating back to the search page. The order is a bit odd, there's a search page, then a submit form page, and the summary page. I have replace set to true in the reach router so when I click back on the summary page it goes to the search page. I want to preserve the history and pass the state of the submitted data into history, so when I click forward it goes back to the page without error.
I've looked up a bunch of guides and went through some of the docs, I think I've got a good idea of how to build this, but in this component we're destructuring props, so how do I pass those into the state variable of history?
export const BaseSummary = ({successState, children}: BaseSummaryProps) => {
let ref = createRef();
const [pdf, setPdf] = useState<any>();
const [finishStatus, setfinishStatus] = useState(false);
const onBackButtonEvent = (e) => {
e.preventDefault();
if (!finishStatus) {
if (window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?")) {
setfinishStatus(true);
props.history.push(ASSOCIATE_POLICY_SEARCH_ROUTE); // HERE
} else {
window.history.pushState({state: {successState: successState}}, "", window.location.pathname);
setfinishStatus(false);
}
}
};
useEffect(() => {
window.history.pushState(null, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
Also I'm not passing in the children var because history does not clone html elements, I just want to pass in the form data that's returned for this component to render the information accordingly
first of all, I think you need to use "useHistory" to handling your hsitry direct without do a lot of complex condition, and you can check more from here
for example:
let history = useHistory();
function handleClick() {
history.push("/home");
}
now, if you need to pass your history via props in this way or via your code, just put it in function and pass function its self, then when you destruct you just need to write your function name...for example:
function handleClick() {
history.push("/home");
}
<MyComponent onClick={handleClick} />
const MyComponent = ({onClick}) => {....}
I fixed it. We're using reach router, so everytime we navigate in our submit forms pages, we use the replace function like so: {replace: true, state: {...stateprops}}. Then I created a common component that overrides the back button functionality, resetting the history stack every time i click back, and using preventdefault to stop it from reloading the page. Then I created a variable to determine whether the window.confirm was pressed, and when it is, I then call history.back().
In some scenarios where we went to external pages outside of the reach router where replace doesn't work, I just used window.history.replaceStack() before the navigate (which is what reach router is essentially doing with their call).
Anyways you wrap this component around wherever you want the back button behavior popup to take effect, and pass in the successState (whatever props you're passing into the current page you're on) in the backButtonBehavior function.
Here is my code:
import React, {useEffect, ReactElement} from 'react';
import { StateProps } from '../Summary/types';
export interface BackButtonBehaviorProps {
children: ReactElement;
successState: StateProps;
}
let isTheBackButtonPressed = false;
export const BackButtonBehavior = ({successState, children}: BackButtonBehaviorProps) => {
const onBackButtonEvent = (e: { preventDefault: () => void; }) => {
e.preventDefault();
if (!isTheBackButtonPressed) {
if (window.confirm("Your claim has been submitted, would you like to exit before getting additional claim information?")) {
isTheBackButtonPressed = true;
window.history.back();
} else {
isTheBackButtonPressed = false;
window.history.pushState({successState: successState}, "success page", window.location.pathname); // When you click back (this refreshes the current instance)
}
} else {
isTheBackButtonPressed = false;
}
};
useEffect(() => {
window.history.pushState(null, "", window.location.pathname);
window.addEventListener('popstate', onBackButtonEvent);
return () => {
window.removeEventListener('popstate', onBackButtonEvent);
};
}, []);
return (children);
};

ReactJS update data in component when user is logged in

I have dashboard which should show data if a user is logged in and other data if no user is logged in. I already managed to figure out if a user is logged in it is not reflected on the page. It only changes after reloading the page.
This is what I have: An Account object with a userstatus component to hold details of the user. The Account object is placed in a context that is wrapped in the App.js. It also has a getSession function which gets the user details from the authentication mechanism. getSession also updates the userstatus according to the result (logged_in or not_logged_in). Second I have a dashboard component which runs the getSession method and puts the result in the console. Everythings fine. But the render function did not get the changed userstatus.
This is my code (Accounts.js):
export const AccountContext = createContext();
export const Account = {
userstatus: {
loggedinStatus: "not_logged_in",
values: {},
touched: {},
errors: {}
},
getSession: async () =>
await new Promise((resolve, reject) => {
...
}
}),
}
This is the Dashboard.js:
const Dashboard = () => {
const [status, setStatus] = useState();
const { getSession, userstatus } = useContext(AccountContext);
getSession()
.then(session => {
console.log('Dashboard Session:', session);
userstatus.loggedinStatus = "logged_in"
setStatus(1)
})
.catch(() => {
console.log('No Session found.');
userstatus.loggedinStatus = "not_logged_in"
setStatus(0);
});
const classes = useStyles();
return (
<div className={classes.root}>
{userstatus.loggedinStatus}
{status}
{userstatus.loggedinStatus === "logged_in" ? 'User logged in': 'not logged in'}
<Grid
container
spacing={4}
...
I already tried with useState and useEffect, both without luck. The userstatus seems to be the most logical, however, it does not update automatically. How can I reflect the current state in the Dashboard (and other components)?
React only re-renders component when any state change occur.
userstatus is simply a variable whose changes does not reflect for react. Either you should use userstatusas your app state or you can pass it in CreateContext and then use reducers for update. Once any of two ways you use, you would see react's render function reflect the changes in userstatus.
For how to use Context API, refer docs

Resources