NextJS - useSWR with token from session - next.js

I'm working with NextJS, Next-auth and Django as backend. I'm using the credentials provider to authenticate users. Users are authenticated against the Django backend and the user info together with the accesstoken is stored in the session.
I'm trying to use useSWR now to fetch data from the backend. (no preloading for this page required, that's why I'm working with SWR) I need to send the access_token from the session in the fetcher method from useSWR. However I don't know how to use useSWR after the session is authenticated. Maybe I need another approach here.
I tried to wait for the session to be authenticated and then afterwards send the request with useSWR, but I get this error: **Error: Rendered more hooks than during the previous render.
**
Could anybody help with a better approach to handle this? What I basically need is to make sure an accesstoken, which I received from a custom backend is included in every request in the Authorization Header. I tried to find something in the documentation of NextJS, Next-Auth or SWR, but I only found ways to store a custom access_token in the session, but not how to include it in the Header of following backend requests.
This is the code of the component:
import { useSession } from "next-auth/react";
import useSWR from 'swr';
import axios from 'axios'
export default function Profile() {
const { data: session, status } = useSession();
// if session is authenticated then fetch data
if (status == "authenticated") {
// create config with access_token for fetcher method
const config = {
headers: { Authorization: `Bearer ${session.access_token}` }
};
const url = "http://mybackend.com/user/"
const fetcher = url => axios.get(url, config).then(res => res.data)
const { data, error } = useSWR(url, fetcher)
}
if (status == "loading") {
return (
<>
<span>Loading...</span>
</>
)
} else {
return (
<>
{data.email}
</>
)
}
}

you don't need to check status every time. what you need to do is to add this function to your app.js file
function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push("/sign-in");
},
});
if (status === "loading") {
return (
<div> Loading... </div>
);
}
return children;
}
then add auth proprety to every page that requires a session
Page.auth = {};
finally update your const App like this
<SessionProvider session={pageProps.session}>
<Layout>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</Layout>
</SessionProvider>
so every page that has .auth will be wrapped with the auth component and this will do the work for it
now get rid of all those if statments checking if session is defined since you page will be rendered only if the session is here

Thanks to #Ahmed Sbai I was able to make it work. The component now looks like this:
import { useSession } from "next-auth/react";
import axios from "axios";
import useSWR from 'swr';
Profile.auth = {}
export default function Profile() {
const { data: session, status } = useSession();
// create config with access_token for fetcher method
const config = {
headers: { Authorization: `Bearer ${session.access_token}` }
};
const url = "http://mybackend.com/user/"
const fetcher = url => axios.get(url, config).then(res => res.data)
const { data, error } = useSWR(url, fetcher)
if (data) {
return (
<>
<span>{data.email}</span>
</>
)
} else {
return (
<>
Loading...
</>
)
}
}
App component and function:
function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push("/api/auth/signin");
},
});
if (status === "loading") {
return (
<div> Loading... </div>
);
}
return children;
}
function MyApp({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={pageProps.session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</SessionProvider>
)
}

Related

Is it ok to use getSession inside getServerSideProps to redirect an already logged in users if they try to access a custom login page?

Please find below the code of my (very simple, for demonstration purposes) custom login page. I am using getSession inside getServerSideProps to determine wheter there is already a session with a user. If that is the case, I redirect to the "root page". If not, I "hydrate" my page with the currently available "providers" as "props".
Is my approach valid? Or is there anything "more best-practice" I could do? And, specifically, is it ok to use getSession inside getServerSideProps in this way?
import { getProviders, getSession, signIn } from 'next-auth/react';
import type { BuiltInProviderType } from 'next-auth/providers';
import type { ClientSafeProvider, LiteralUnion } from 'next-auth/react';
import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { Session } from 'next-auth';
interface Properties {
providers: Record<
LiteralUnion<BuiltInProviderType, string>,
ClientSafeProvider
> | null;
}
export default function SignIn({ providers }: Properties) {
return (
<>
{providers &&
Object.values(providers).map((provider) => (
<div key={provider.name}>
<button
onClick={() => signIn(provider.id, { callbackUrl: '/test' })}
>
Sign in with {provider.name}
</button>
</div>
))}
</>
);
}
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const session: Session | null = await getSession({ req: context.req });
if (session && session.user) {
console.log(
'Since there is already an active session with a user you will be redirected!'
);
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
return { props: { providers: await getProviders() } };
};

How to redirect using `getServerSideProps` with props in Next.js?

After a user signs in, I use router.push() to redirect the user to their profile page. I am using getServerSideProps() for authentication right now. When the redirect happens, the props don't seem to be fetched and I have to refresh the browser myself to call gSSR. Is this behavior normal or is there a way to fix it?
Demonstration - Updated
login.js
import {useRouter} from 'next/router';
export default function Login({user}) {
const router = useRouter();
// invoked on submission
async function submitLoginForm(email, password) {
const user = await signIn(email, password)
const username = await getUsernameFromDB(user);
router.push("/" + username);
}
return ( ... );
}
export async function getServerSideProps({req}) {
const user = await getUserFromCookie(req);
if(user === null) {
return {
props: {}
}
}
return {
redirect: {
destination: `/${user.username}`,
permanent: false
}
}
}
[username].js
export default function Profile({user, isUser}) {
// Use isUser to render different interface.
return ( ... );
}
export async function getServerSideProps({params, req}) {
// The username of the path.
const profileUsername = params.username
// Current user.
const user = await getUserFromCookie(req);
...
if(user !== null) {
return {
props: {
user: user,
isUser: user !== null && profileUsername === user.username
}
}
}
return {
notFound: true
}
}
The cookie is set in the _app.js using the Supabase auth sdk.
function MyApp({Component, pageProps}) {
supabase.auth.onAuthStateChange( ( event, session ) => {
fetch( "/api/auth", {
method: "POST",
headers: new Headers( { "Content-Type": "application/json" } ),
credentials: "same-origin",
body: JSON.stringify( { event, session } ),
} );
} );
return (
<Component {...pageProps} />
);
}
I would recommend that you update your _app.js like that:
import { useEffect } from 'react';
function MyApp({ Component, pageProps }) {
// make sure to run this code only once per application lifetime
useEffect(() => {
// might return an unsubscribe handler
return supabase.auth.onAuthStateChange(( event, session ) => {
fetch( "/api/auth", {
method: "POST",
headers: new Headers( { "Content-Type": "application/json" } ),
credentials: "same-origin",
body: JSON.stringify( { event, session } ),
});
});
}, []);
return <Component {...pageProps} />;
}
Also, please make clear what is happening. E.g. my current expectation:
Not authenticated user opens the "/login" page
He does some login against a backend, that sets a cookie value with user information
Then router.push("/" + username); is called
But the problem now: On page "/foo" he sees now the Not-Found page instead of the user profile
Only after page reload, you see the profile page correctly
If the above is correct, then it is possible the following line is not correctly awaiting the cookie to be persisted before the navigation happens:
const user = await signIn(email, password)
It could be that some internal promise is not correctly chained/awaited.
As an recommendation, I would log to the console the current cookie value before calling the router.push to see if the cookie was already saved.

Nextjs + Apollo-client, getServerSideProps : prop data is not updated in production

When I test getServerSideProps() in development mode, prop data in my landing page is updated constantly, because the app is under fast build mode.
But when the app is deployed in vercel, the data in landing page is not updated even though my database (mongoDB) has been updated.
If I check Heroku logs (where backend is deployed), there is no POST request by client when I (user) visit landing page second time. Therefore, I am assuming that my browser uses the cached html page and not sending request to server.
Could anyone help me to explain what is the issue?
import { ApolloClient, InMemoryCache } from "#apollo/client";
import { GetServerSideProps } from "next";
import { GetAllPosts as query } from "#network/queries";
const client = new ApolloClient({
uri: //my backend uri,
cache: new InMemoryCache(),
});
//pages/_app.ts
import type { AppProps } from "next/app";
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={client}>
<AuthContext.Provider value={authService}>
<Component {...pageProps} />
</AuthContext.Provider>
</ApolloProvider>
);
}
//pages/index.tsx
export const getServerSideProps: GetServerSideProps = async () => {
let posts = [];
try {
const {
data: { getAllPosts },
} = await client.query({ query });
posts = !!getAllPosts.length && getAllPosts;
} catch (err) {
console.error(`----------error --------- ${err}`);
} finally {
return {
props: {
posts,
},
};
}
};
export default function Landing({ posts }: Props) {
////// react code
}

Next.js: How to clear browser history with Next Router?

I created a wrapper for the pages which will bounce unauthenticated users to the login page.
PrivateRoute Wrapper:
import { useRouter } from 'next/router'
import { useUser } from '../../lib/hooks'
import Login from '../../pages/login'
const withAuth = Component => {
const Auth = (props) => {
const { user } = useUser();
const router = useRouter();
if (user === null && typeof window !== 'undefined') {
return (
<Login />
);
}
return (
<Component {...props} />
);
};
if (Component.getInitialProps) {
Auth.getInitialProps = Component.getInitialProps;
}
return Auth;
};
export default withAuth;
That works \o/, However I noticed a behavior when I log out, using Router.push('/',), to return the user to the homepage the back button contains the state of previous routes, I want the state to reset, as a user who is not authenticated should have an experience as if they're starting from scratch...
Thank you in advance!
You can always use Router.replace('/any-route') and the user will not be able to go back with back button

Next JS fetch data once to display on all pages

This page is the most relevant information I can find but it isn't enough.
I have a generic component that displays an appbar for my site. This appbar displays a user avatar that comes from a separate API which I store in the users session. My problem is that anytime I change pages through next/link the avatar disappears unless I implement getServerSideProps on every single page of my application to access the session which seems wasteful.
I have found that I can implement getInitialProps in _app.js like so to gather information
MyApp.getInitialProps = async ({ Component, ctx }) => {
await applySession(ctx.req, ctx.res);
if(!ctx.req.session.hasOwnProperty('user')) {
return {
user: {
avatar: null,
username: null
}
}
}
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return {
user: {
avatar: `https://cdn.discordapp.com/avatars/${ctx.req.session.user.id}/${ctx.req.session.user.avatar}`,
username: ctx.req.session.user.username
},
pageProps
}
}
I think what's happening is this is being called client side on page changes where the session of course doesn't exist which results in nothing being sent to props and the avatar not being displayed. I thought that maybe I could solve this with local storage if I can differentiate when this is being called on the server or client side but I want to know if there are more elegant solutions.
I managed to solve this by creating a state in my _app.js and then setting the state in a useEffect like this
function MyApp({ Component, pageProps, user }) {
const [userInfo, setUserInfo] = React.useState({});
React.useEffect(() => {
if(user.avatar) {
setUserInfo(user);
}
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<NavDrawer user={userInfo} />
<Component {...pageProps} />
</ThemeProvider>
);
}
Now the user variable is only set once and it's sent to my NavDrawer bar on page changes as well.
My solution for this using getServerSideProps() in _app.tsx:
// _app.tsx:
export type AppContextType = {
navigation: NavigationParentCollection
}
export const AppContext = createContext<AppContextType>(null)
function App({ Component, pageProps, navigation }) {
const appData = { navigation }
return (
<>
<AppContext.Provider value={appData}>
<Layout>
<Component {...pageProps} />
</Layout>
</AppContext.Provider>
</>
)
}
App.getInitialProps = async function () {
// Fetch the data and pass it into the App
return {
navigation: await getNavigation()
}
}
export default App
Then anywhere inside the app:
const { navigation } = useContext(AppContext)
To learn more about useContext check out the React docs here.

Resources