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();
Related
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>
}
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);
useSession((s) => s.user) will either return an user object or null. On the index page the name of the user should be shown. The page is wrapped in PrivatePageProvider which will redirect the client if the user object is null. So if the user is null, IndexPage should not even be rendered, instead the provider returns null and redirects. However this is not working and a TypeError gets thrown, because IndexPage tries to access name of null. With react-router this approach works, but not with Next. I know I could just use the optional chaining operator, but I want to assume that user is not null if the page is wrapped in PrivatePageProvider.
function PrivatePageProvider({ children }) {
const user = useSession((s) => s.user);
const router = useRouter();
useEffect(() => {
if (!user) {
router.push('/auth/login');
}
}, [user, router]);
return !user ? null : children;
}
function IndexPage() {
const user = useSession((s) => s.user);
return (
<PrivatePageProvider>
<h1>Hello {user.name}</h1>
</PrivatePageProvider>
);
}
Error:
TypeError: Cannot read properties of null (reading 'name')
Why is this error showing up even though IndexPage does not get rendered? How can I get my approach to work without involving the server side?
Why is this error showing up even though IndexPage does not get rendered? > Because it's breaking, you have an error, it won't render.
How can I get my approach to work without involving the server side? > Wrap your code in a useEffect. useEffect will only execute client side so anything being rendered server side will just ignore the effect and wait till it's rendered before executing it. That's because any effects will only execute once the DOM has been rendered. If user is null on the server it will break on user.name unless you move that to an effect.
Additionally, I'm quite confused by what exactly you're trying to do, why is there a useEffect in the child component at all, instead you should be doing that in IndexPage. I'm also not sure what the PrivatePageProvider is for at all, it would make more sense if the page provider was rendering the index page, the other way around. You also shouldn't pass the user as children, instead pass the data prop and render the user in the child page, then you don't need to get the user again.
That being said, looking at your code, it seems to me like you were trying to do something and then got confused/lost along the way by accidentally swapping around your thinking with the way you're using the page and page provider. Perhaps this is what you wanted:
const PrivatePageProvider = ({children}) => {
const user = useSession((s) => s.user)
const router = useRouter()
useEffect(() => {
if (!user) {
router.push('/auth/login')
}
}, [user])
return !user ? <></> : children
}
const IndexPage = () => {
const user = useSession((s) => s.user)
return <h1>Hello {user.name}</h1>
}
So that you can use it like:
<PrivatePageProvider>
<IndexPage/>
</PrivatePageProvider>
<PrivatePageProvider>
<ExamplePage/>
</PrivatePageProvider>
I don't think this approach will work on react-router either. The error is about the context javascript knows when executing your code. To understand better, the JSX tree you are rendering transpiled to plain js looks like this:
function IndexPage() {
const user = useSession((s) => s.user);
return (React.createElement(PrivatePageProvider, null,
React.createElement("h1", null,
"Hello ",
user.name)));
}
When javascript executes a function, it evaluates the arguments first; in this case, when it sees one of them is trying to access a property on a null object(user.name) it throws the TypeError without even calling the function.
The situation is different if you pass the whole user object as a prop to another element
function IndexPage() {
const user = useSession((s) => s.user);
return (
<PrivatePageProvider>
<SomeComponent user={user} />
</PrivatePageProvider>
);
}
It transpiles to
function IndexPage() {
const user = useSession((s) => s.user);
return (React.createElement(PrivatePageProvider, null,
React.createElement(SomeComponent, { user: user })));
}
Here javascript has no idea that you will access user.name inside SomeComponent, hence it continues with the execution without erroring
I suggest using the Context API to provide a non-null user to the components wrapped in your PrivatePageProvider
const UserContext = React.createContext({});
const useUser = () => {
const { user } = useContext(UserContext);
return user;
};
function PrivatePageProvider({ children }: any) {
const user = useSession((s) => s.user);
...
return !user ? null : (
<UserContext.Provider value={{ user }}>{children}</UserContext.Provider>
);
}
function SomeComponent() {
const user = useUser();
return <h1>{user.name}</h1>;
}
function IndexPage() {
return (
<PrivatePageProvider>
<SomeComponent />
</PrivatePageProvider>
);
}
I had a similar problem with my project.
I used nextJS with next-auth that next-auth provided loading state. I don't know how do you get user data but you should get loading state in addition to user and handle authentication using loading. like this :
function PrivatePageProvider({ children }) {
const [session, loading] = useSession();
const router = useRouter();
const isAuthenticate = !!user;
useEffect(() => {
if (loading) return; // Do nothing while loading
if (!isAuthenticate) {
router.push('/auth/login'); // If not authenticated, force log in
}
}, [user, loading]);
if(isAuthenticate) {
return <>children</>
}
// Session is being fetched, or no user.
// If no user, useEffect() will redirect.
return <span>... loading</span>;
}
I put next-auth package address if you like to use it:
next-auth
For a React project, I'm using multiple context providers running GraphQL queries, providing all, or some components with my needed data. For this example, I have a BookProvider that queries all the books for a specific user.
export const BookProvider = ({ children }) => {
// GraphQL query for retrieving the user-belonged books
const { loading, data } = useQuery(gql`{
books {
id,
name,
description
}
}
`, {
pollInterval: 500
})
if(!data?.books) return null
return (
<BookContext.Provider value={data?.books}>
{!loading && children}
</BookContext.Provider>
)
}
I made sure the query wasn't loading and is available in the data object. For retreiving my data, I have a dashboard component wrapped in this provider. In here, there are other components loading the corresponding data.
const Dashboard = () => {
return (
<BookProvider>
// More components for loading data
</BookProvider>
)
}
The data is loaded like this:
const { id, name } = useContext(BookContext)
In the same way, I have a AuthenticationProvider, wrapping the whole application.
export const MeQuery = gql`{ me { id, email } }`
export const AuthenticationProvider = (props) => {
const { loading, data } = useQuery(MeQuery)
if(loading) return null
const value = {
user: (data && data.me) || null
}
return <AuthenticationContext.Provider value={value} {...props} />
}
For my application routes, checking if the user is authenticated, I use a PrivateRoute component. The Dashboard component is loaded by this PrivateRoute.
const PrivateRoute = ({ component: Component, ...rest }) => {
const { user } = useAuthentication()
return user ? <Route {...rest} render={props => <Component {...props} />} /> : <Redirect to={{ pathname: '/login' }} />
}
When logging in, and setting the user object, there is no problem, however, when logging out, which is a component on the Dashboard, it re-directs me to the login page, but I receive the following error:
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at BookProvider (http://localhost:3000/static/js/main.chunk.js:2869:3)
at Dashboard
at Route (http://localhost:3000/static/js/vendors~main.chunk.js:85417:29)
at PrivateRoute (http://localhost:3000/static/js/main.chunk.js:4180:14)
at Switch (http://localhost:3000/static/js/vendors~main.chunk.js:85619:29)
at Router (http://localhost:3000/static/js/vendors~main.chunk.js:85052:30)
at BrowserRouter (http://localhost:3000/static/js/vendors~main.chunk.js:84672:35)
at Router
For logging out, I'm using the following code:
export function useLogout() {
const client = useApolloClient()
const logout = useMutation(gql`mutation Logout { logout }`, { update: async cache => {
cache.writeQuery({ query: MeQuery, data: { me: null }})
await client.resetStore()
}})
return logout
}
export default useLogout
This gets called like const [logout] = useLogout()
Questions
Why is this error occuring?
How can this be fixed?
Is it a good practice running queries in context providers?
I'll give it another try ;-)
You would use useLazyQuery instead, that seem to allow avoiding the bug that plagues useQuery:
const [ executeQuery, { data, loading } ] = useLazyQuery(gql`${QUERY}`);
And then manage the polling manually:
useEffect(function managePolling() {
const timeout = null
const schedulePolling = () => {
timeout = setTimeout(() => {
executeQuery()
schedulePolling()
}, 500)
}
executeQuery()
schedulePolling()
return () => clearTimeout(timeout)
}, [executeQuery])
(untested code, but you get the idea)
This was suggested on the same thread (https://github.com/apollographql/apollo-client/issues/6209#issuecomment-676373050) so maybe you already tried it...
It seems that your graphql query hook is polling data, and this will update its internal state, or try to do so, even if the component is unmounted.
I've read similar issues in that thread (and it looks like a bug because useQuery hook should detect when component is unmounted and stop polling)
One of the solutions that seems to fit your issue, is to control the polling so that you can stop it once the component unmounts.
It would look like so:
export const BookProvider = ({ children }) => {
// GraphQL query for retrieving the user-belonged books
const { loading, data, stopPolling, startPolling } = useQuery(gql`{
books {
id,
name,
description
}
}`, {fetchPolicy: "network-only"})
React.useEffect(function managePolling() {
startPolling(500)
return () => stopPolling()
})
if(!data?.books) return null
return (
<BookContext.Provider value={data?.books}>
{!loading && children}
</BookContext.Provider>
)
}
Does it suppress the warning?
Note: not sure if the {fetchPolicy: "network-only"} option is needed
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