I am using the official with-apollo example to create a nextjs frontend. I am trying to use the user's slug, which can be found in the url string to render the user profile. However, I am not able to use the url parameter (the slug) as a variable in the graphql query.
The Link to the user profile
<Link href={{ pathname: "/users/[slug]", query: { slug: user.slug } }}>
The user profile component
import { gql, useQuery } from "#apollo/client"
import ErrorMessage from "./ErrorMessage"
import { useRouter } from "next/router";
export const USER_QUERY = gql`
query getUser($slug: String!) {
user(slug: $slug) {
id
email
}
}
`
// I can not get this to work using url parameters
export const userQueryVars = {
slug: "userSlug", // This should be a url parameter!!
}
export default function UserProfile() {
const router = useRouter()
const userSlug = router.query.slug
const { loading, error, data } = useQuery(USER_QUERY, {
variables: {slug: userSlug},
})
if (error) return <ErrorMessage message="Error loading users." />
if (loading) return <div>Loading</div>
if (!data) return <div>No data</div>
const { user } = data
return (
<section>
<div>
<h3>
{user.firstName} {user.lastName}
</h3>
<p>{user.email}</p>
</div>
</section>
)
}
The user profile page
import App from "../../../components/App"
import Header from "../../../components/Header"
import UserProfile, {
USER_QUERY,
userQueryVars,
} from "../../../components/UserProfile"
import { initializeApollo, addApolloState } from "../../../lib/apolloClient"
const UserProfilePage = () => (
<App>
<Header />
<UserProfile />
</App>
)
export async function getServerSideProps() {
const apolloClient = initializeApollo()
await apolloClient.query({
query: USER_QUERY,
variables: userQueryVars, // This is passed from the component!
})
return addApolloState(apolloClient, {
props: {}
})
}
export default UserProfilePage
What I have tried so far (among a lot of other things):
Using router:
export const userQueryVars = {
slug: router.query.slug,
}
Error: You should only use "next/router" inside the client side of your app.
Using router and checking that is it called on client side:
if (process.browser) {
export const userQueryVars = {
slug: router.query.slug,
}
}
Error: 'import' and 'export' may only appear at the top level.
I would be very thankful for any kind of help!!
When using getServerSideProps you can find your slug (and all other dynamic params if you have them) inside context.params:
export async function getServerSideProps(context) {
const { slug } = context.params;
// Do whatever you need with `slug`
// ...
}
Related
I am working with Reactjs and Nextjs and i am working on dynamic routes,In other words i have list of blogs and now i want to display blog details for this i created folder name "blogs" and put file name "[slug.js"] inside this but unable to redirect to that url, Here is my current code in Allblog.tsx
<Link href={`/blog/${todoList.id}`}>Edit</Link>
And here is my code inside "blog/[slug.js]"
import Axios from "axios";
import {useRouter} from "next/router";
import { Editor } from '#tinymce/tinymce-react';
//import LatestBlogs from "../../components/LatestBlogs/LatestBlogs";
import Link from 'next/link'
import { useEffect, useState } from 'react'
import Sidebar from '../../components/Sidebar'
const slug = ({ posts }) => {
return(
<div>
<h2> Hello World </h2>
</div>
);
};
export default slug;
export const getStaticProps = async ({ params }) => {
const { data } = await Axios.get(`https://xxxxxxxxxxxxxx/api/getblogbyuserid/${params.slug}`);
const post = data;
return {
props: {
post,
},
};
};
export const getStaticPaths = async () => {
const { data } = await Axios.get("http://xxxxxxxxxxxxxxxxx/api/blogs");
const posts = data.slice(0, 10);
const paths = posts.map((post) => ({ params: { slug: post.id.toString() } }));
return {
paths,
fallback: true,
};
};
file name should be blog/[slug].js
Next JS 13 was just released last month and totally changed the way data was fetched while also providing an alternative to the use of _app.js and _document.js with the use of the root layout.js. Previously in Next JS 12 and below, to use the React Query SSR feature using the Hydration method, you needed to set your _app.js file like this:
import { Hydrate, QueryClient, QueryClientProvider } from '#tanstack/react-query';
import queryClientConfig from '../queryClientConfig';
export default function MyApp({ Component, pageProps }) {
const queryClient = useRef(new QueryClient(queryClientConfig));
const [mounted, setMounted] = useState(false);
const getLayout = Component.getLayout || ((page) => page);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<ErrorBoundary FallbackComponent={ErrorFallbackComponent}>
<QueryClientProvider client={queryClient.current}>
<Hydrate state={pageProps.dehydratedState}>
<AppProvider>
{getLayout(<Component {...pageProps} />)}
</AppProvider>
</Hydrate>
</QueryClientProvider>
</ErrorBoundary>
);
}
To utilize React Query SSR in a page in Next JS using getServerSideProps, it goes like this:
// Packages
import Head from 'next/head';
import { dehydrate, QueryClient } from '#tanstack/react-query';
// Layout
import getDashboardLayout from '../../layouts/dashboard';
// Parse Cookies
import parseCookies from '../../libs/parseCookies';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
import { fetchUserProfile } from '../../hooks/user/api';
import { getGoogleAuthUrlForNewAccount } from '../../hooks/auth/api';
import { fetchCalendarsOnServer } from '../../hooks/event/api';
import { useFetchCalendars } from '../../hooks/event';
// Store
import useStaticStore from '../../store/staticStore';
// `getServerSideProps function`
export async function getServerSideProps({ req, res }) {
const cookies = parseCookies(req);
const queryClient = new QueryClient();
try {
await queryClient.prefetchQuery(['fetchUserProfile'], () =>
fetchUserProfile(cookies.userAccessToken)
);
await queryClient.prefetchQuery(['fetchCalendars'], () => fetchCalendarsOnServer(cookies.userAccessToken));
await queryClient.prefetchQuery(['getGoogleAuthUrlForNewAccount'], () =>
getGoogleAuthUrlForNewAccount(cookies.userAccessToken)
);
} catch (error) {
}
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}
function Home() {
const {
data: userProfileData, // This data is immediately made available without any loading as a result of the hydration and fetching that has occurred in `getServerSideProps`
isLoading: isUserProfileDataLoading,
error: userProfileDataError,
} = useFetchUserProfile();
const { data: savedCalendarsData } = useFetchCalendars(); // This data is immediately made available without any loading as a result of the hydration and fetching that has occurred in `getServerSideProps`
return (
<>
<Head>
<title>
{userProfileData.data.firstName} {userProfileData.data.lastName} Dashboard
</title>
<meta
name="description"
content={`${userProfileData.data.firstName} ${userProfileData.data.lastName} Dashboard`}
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<PageContentWrapper
>
Page Content
</PageContentWrapper>
</>
);
}
Home.getLayout = getDashboardLayout; // This layout also needs data from userProfileData to be available. There is no problem and it never loads because the data is immediately available on mount.
export default Home;
Here is the old DashboardLayout component:
// Packages
import PropTypes from 'prop-types';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
};
function DashboardLayout({ children }) {
const { isLoading, error, data: userProfileData } = useFetchUserProfile(); // Data is immediately available and never loads because it has been fetched using SSR in getServerSideProps
if (isLoading)
return (
<div className="w-screen h-screen flex items-center justify-center text-white text-3xl font-medium">
Loading...
</div>
);
if (error) {
return (
<div className="w-screen h-screen flex items-center justify-center text-brand-red-300 text-3xl font-medium">
{error.message}
</div>
);
}
return (
<>
<div className="the-dashboard-layout">
{/* Start of Main Page */}
<p className="mb-2 text-brand-gray-300 text-sm leading-5 font-normal">
<span className="capitalize">{`${userProfileData.data.firstName}'s`}</span> Layout
</p>
</div>
</>
);
}
export default function getDashboardLayout(page) {
return <DashboardLayout>{page}</DashboardLayout>;
}
Using the new Next JS 13, there is no way to use the React Query Hydration method and even when I am able to fetch the data using the new method, the data is still refetched when the component mounts which causes the layout to be in a loading state as the data is not immediately available.
In Next 13, you only need to call the data fetching method and pass it to the client components directly because the app directory now supports server components directly.
First of all, a root layout file replaces the old _app.js and _document.js file in Next 13: It is important to note that there is no pageProps object anymore for the dehydratedState.
Here is the RootLayout server component:
// Packages
import PropTypes from 'prop-types';
// Components
import RootLayoutClient from './root-layout-client';
RootLayout.propTypes = {
children: PropTypes.node.isRequired,
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<RootLayoutClient>{children}</RootLayoutClient>
</body>
</html>
);
}
Here is the RootLayoutClient client component for the layout needed because of the use of Context and State which are client-side operations:
'use client';
// Packages
import React, { useRef, useEffect } from 'react';
import { QueryClient, QueryClientProvider } from '#tanstack/react-query';
import PropTypes from 'prop-types';
// Context
import { AppProvider } from '../contexts/AppContext';
// Config
import queryClientConfig from '../queryClientConfig';
RootLayoutClient.propTypes = {
children: PropTypes.node.isRequired,
};
export default function RootLayoutClient({ children }) {
const queryClient = useRef(new QueryClient(queryClientConfig));
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<QueryClientProvider client={queryClient.current}>
<AppProvider>
{children}
</AppProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
getServerSideProps method has now been replaced with normal Promise based data fetching method using the FETCH API. The fetched data returned can now passed into the page/component that needs it.
Here is my data fetching function:
import { getCookie } from 'cookies-next';
export const fetchUserProfile = async (token) => {
if (token) {
try {
const response = await fetch(process.env.NEXT_PUBLIC_EXTERNAL_API_URL + FETCH_USER_PROFILE_URL, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
return data;
} else {
Promise.reject(response);
}
} catch (error) {
Promise.reject(error);
}
} else {
try {
const response = await fetch(process.env.NEXT_PUBLIC_EXTERNAL_API_URL + FETCH_USER_PROFILE_URL, {
method: 'GET',
headers: {
Authorization: `Bearer ${getCookie('userAccessToken')}`,
},
});
if (response.ok) {
const data = await response.json();
return data;
} else {
Promise.reject(response);
}
} catch (error) {
Promise.reject(error);
}
}
};
Here is how the data is fetched and used in the home page. Note that the home page resides in the app directory: home/page.js:
import { cookies } from 'next/headers';
// Hooks
import { fetchUserProfile } from '../../../hooks/user/api';
// Components
import HomeClient from './home-client';
export default async function Page() {
const nextCookies = cookies();
const userAccessToken = nextCookies.get('accessToken');
const userProfileData = await fetchUserProfile(userAccessToken.value);
// This is essentially prop passing which was not needed using the previous hydration and getServerSideProps methods.
// Now, I have to pass this data down to a client component called `HomeClient` that needs the data. This is done because I may need to perform some client-side operations on the component.
return <HomeClient userData={userProfileData} />;
}
Here is the HomeClient client component:
'use client';
import { useEffect } from 'react';
import PropTypes from 'prop-types';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
HomeClient.propTypes = {
userData: PropTypes.any.isRequired,
calendarData: PropTypes.any.isRequired,
};
export default function HomeClient({ userData }) {
const { isLoading, error, data: userProfileData } = useFetchUserProfile();
useEffect(() => {
console.log(JSON.stringify(userData));
}, [userData]);
// This now loads instead of being immediately available. This can be mitigated by directly using the userData passed
// through props but I don't want to engage in prop drilling in case I need it to be passed into deeper nested child components
if (isLoading) {
return (
<div>Loading...</div>
)
}
return (
<>
<AnotherChildComponent profileData={userProfileData.data.profile}/>
</>
);
}
Here is the useFetchUserProfile hook function used in HomeClient client component above:
export const useFetchUserProfile = (conditional = true) => {
// Used to be immediately available as a result of the key 'fetchUserProfile' being used to fetch data on getServerSideProps but that's not available in the app directory
return useQuery(['fetchUserProfile'], () => fetchUserProfile(), {
enabled: conditional,
cacheTime: 1000 * 60 * 5,
});
};
Here is the parent layout.js file required by NextJS 13 to share a common layout. This layout.js also needs the fetched data but there is no way to pass the data to this even through props. In the past, Data was immediately available here because of the react-query hydration performed in getServerSideProps
// Packages
import PropTypes from 'prop-types';
// Hooks
import { useFetchUserProfile } from '../../hooks/user';
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
};
function DashboardLayout({ children }) {
const { isLoading, error, data: userProfileData } = useFetchUserProfile();
// Used to be that data was immediately available and never loaded because it has been fetched using SSR in getServerSideProps
// Now, it has to load the same data. This is even more complex because props can't be passed as there is no way or any abstraction method
// to share data between the layout and child components
if (isLoading)
return (
<div className="w-screen h-screen flex items-center justify-center text-white text-3xl font-medium">
Loading...
</div>
);
if (error) {
return (
<div className="w-screen h-screen flex items-center justify-center text-brand-red-300 text-3xl font-medium">
{error.message}
</div>
);
}
return (
<>
<div className="the-dashboard-layout">
{/* Start of Main Page */}
<p className="mb-2 text-brand-gray-300 text-sm leading-5 font-normal">
<span className="capitalize">{`${userProfileData.data.firstName}'s`}</span> Layout
</p>
</div>
</>
);
}
How can I de-duplicate the multiple requests being made and make the data available in all components that fetch the same data without prop-drilling?
And how do I also get around the limitation of not being able to pass the data to the parent layout components even if I wanted to use props.
Thank you in advance.
I have a simple project that I built that protects the routes/pages of the website by using the if and else statement and putting each page with a function withAuth(), but I'm not sure if that is the best way to protect routes with nextjs, and I noticed that there is a delay in protecting the route or pages, like 2-3 seconds long, in which they can see the content of the page before it redirects the visitor or unregistered user to the login page.
Is there a way to get rid of it or make the request faster so that unregistered users don't view the page's content? Is there a better approach to safeguard a certain route in the nextjs framework?
Code
import { useContext, useEffect } from "react";
import { AuthContext } from "#context/auth";
import Router from "next/router";
const withAuth = (Component) => {
const Auth = (props) => {
const { user } = useContext(AuthContext);
useEffect(() => {
if (!user) Router.push("/login");
});
return <Component {...props} />;
};
return Auth;
};
export default withAuth;
Sample of the use of withAuth
import React from "react";
import withAuth from "./withAuth";
function sample() {
return <div>This is a protected page</div>;
}
export default withAuth(sample);
you can make the authentication of user on server-side, if a user is logged in then show them the content of the protected route else redirect them to some other route. refer to this page for mote info.
in getServerSideProps check whether the user has logged in
if (!data.username) {
return {
redirect: {
destination: '/accounts/login',
permanent: false,
},
}
}
here's complete example of protected route page
export default function SomeComponent() {
// some content
}
export async function getServerSideProps({ req }) {
const { token } = cookie.parse(req.headers.cookie)
const userRes = await fetch(`${URL}/api/user`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
const data = await userRes.json()
// does not allow access to page if not logged in
if (!data.username) {
return {
redirect: {
destination: '/accounts/login',
permanent: false,
},
}
}
return {
props: { data }
}
}
With Customized 401 Page
We are going to first define our customized 401 page
import React from "react"
const Page401 = () => {
return (
<React.Fragment>
//code of your customized 401 page
</React.Fragment>
)
}
export default Page401
Now, we are going to change a small part of the code kiranr shared
export async function getServerSideProps({ req }) {
const { token } = cookie.parse(req.headers.cookie)
const userRes = await fetch(`${URL}/api/user`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
const data = await userRes.json()
// does not allow access to page if not logged in
if (!data.username) {
//THIS PART CHANGES
return {
props: {
unauthorized: true
}
}
//THIS PART CHANGES
}
return {
props: { data }
}
}
Then we will check this 'unauthorized' property in our _app.js file and call our customized 401 page component if its value is true
import Page401 from "../components/Error/Server/401/index";
const App = ({ Component, pageProps }) => {
//code..
if (pageProps.unauthorized) {
//if code block reaches here then it means the user is not authorized
return <Page401 />;
}
//code..
//if code block reaches here then it means the user is authorized
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
Please refer to the code below:
Auth.tsx
import { createContext, useEffect, useState, useContext, FC } from 'react';
interface Props {
// any props that come into the component
}
export const PUBLIC_ROUTES = ['/', '/admin/login'];
export const isBrowser = () => typeof window !== 'undefined';
const AuthContext = createContext({
isAuthenticated: false,
isLoading: false,
user: {},
});
export const useAuth = () => useContext(AuthContext);
export const AuthProvider: FC<Props> = ({ children }) => {
const [user, setUser] = useState({});
const [isLoading, setLoading] = useState(true);
useEffect(() => {
async function loadUserFromCookies() {
// const token = Cookies.get('token');
// TODO: Get the token from the cookie
const token = true;
if (token) {
// console.log("Got a token in the cookies, let's see if it is valid");
// api.defaults.headers.Authorization = `Bearer ${token}`;
// const { data: user } = await api.get('users/me');
// if (user) setUser(user);
}
setLoading(false);
}
loadUserFromCookies();
}, []);
return (
<AuthContext.Provider
value={{ isAuthenticated: !!Object.keys(user).length, user, isLoading }}
>
{children}
</AuthContext.Provider>
);
};
export const ProtectRoute: FC<Props> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Loading</div>;
}
if (PUBLIC_ROUTES.includes(window.location.pathname)) {
return <>{children}</>;
}
// If the user is not on the browser or not authenticated
if (!isBrowser() || !isAuthenticated) {
window.location.replace('/login');
return null;
}
return <>{children}</>;
};
_app.tsx
import React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/dist/next-server/lib/router/router';
import { ThemeProvider, StyledEngineProvider } from '#material-ui/core/styles';
import CssBaseline from '#material-ui/core/CssBaseline';
import theme from '../utils/theme';
import { AuthProvider, ProtectRoute } from 'contexts/auth';
export default function MyApp(props: AppProps) {
const { Component, pageProps } = props;
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles && jssStyles.parentElement) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
return (
<React.Fragment>
<Head>
<title>Next App</title>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<AuthProvider>
<ProtectRoute>
<Component {...pageProps} />
</ProtectRoute>
</AuthProvider>
</ThemeProvider>
</StyledEngineProvider>
</React.Fragment>
);
}
Problems:
So, as per the code, if the user is not logged in, redirect the user
to the login page. But, due to the current logic, 404-page
routes are also redirected to the admin login page. How can I catch
the 404 status to redirect to the 404 pages before verifying if the
user is logged in or not?
I am using an array to verify if the path is public or not. Is there
a better way to render public paths without maintaining hard-coded
page paths or using HOCs? The issue with my approach is if any developer changes the file name and if it doesn't match the string in the array, it will not be a public route. If I create a folder called public and all public pages inside it, I get an unnecessary public/ in my URL. I do not want to use HOCs because I have to import them every time I create a new page which isn't incorrect but I am expecting a better approach.
Thank you.
You can use HOC (Higher Order Components) to wrap the paths which:
can only accessed with authorization;
can only be access when
user unauthenticated (e.g /login -> you don't want user to
access /login page when he's already logged in)
Like so:
login.js -> /login
import withoutAuth from 'path/withoutAuth';
function Login() {
return (<>YOUR COMPONENT</>);
};
export default withoutAuth(Login);
Same can be done with withAuth.js -> HOC (Higher Order Component) which wraps components which can only accessed with authentication
protectedPage.js -> /protectedPage
import withAuth from 'path/withAuth';
function ProtectedPage() {
return (<>YOUR COMPONENT</>);
};
export default withAuth(ProtectedPage);
You can how you can make these HOCs in this article.
Setting up authentication and withConditionalRedirect (HOC which will be used to make withAuth & withoutAuth HOCs): Detecting Authentication Client-Side in Next.js with an HttpOnly Cookie When Using SSR.
Making withAuth & withoutAuth HOCs: Detecting a User's Authenticated State Client-Side in Next.js using an HttpOnly Cookie and Static Optimization
I am using the react-spotify-login package and when trying to authorize the application I can't retrieve the access token. My routing works and sending the request works. I just can't retrieve the token. I've just started learning react so I'm hoping it isn't something I'm easily overlooking.
import React, { Component } from 'react';
import SpotifyLogin from 'react-spotify-login';
import { clientId, redirectUri } from '../../Settings';
import { Redirect } from 'react-router-dom';
export class Login extends Component {
render() {
const onSuccess = ({ response }) => {
//const { access_token: token } = response;
console.log("[onSuccess]" + response);
return <Redirect to='/home' />
};
const onFailure = response => console.error("[onFailure]" + response);
return (
<div>
<SpotifyLogin
clientId={clientId}
redirectUri={redirectUri}
onSuccess={onSuccess}
onFailure={onFailure}
/>
</div>
);
}
}
export default Login;
In your approach you are trying to destructure the response data/object and pull field 'response' which does not exist i.e undefined
Change
const onSuccess = ({ response }) => {
to
const onSuccess = (response) => {