I'm working on my first integration of Sanity with Next.Js, trying to add a blog to a personal site. Dev works fine, but when I go to deploy, or do a build, I get an error that it can't find one of the props for the blog page.
Error thrown is this:
TypeError: Cannot read property 'title' of undefined
This is what I'm using for my [slug].js file:
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Layout from '../../components/layout';
import Scrollbar from 'react-scrollbars-custom';
import Transitions from '../../lib/transitions';
import BlockContent from '#sanity/block-content-to-react';
import { postQuery, postSlugsQuery } from '../../lib/grocQueries';
import { getClient, overlayDrafts, sanityClient } from '../../lib/sanity.server';
import { urlForImage, usePreviewSubscription } from '../../lib/sanity';
const pageVariants = Transitions.pageVariant;
const pageTransition = Transitions.pageTransition;
export const Post = ({ data = {}, preview }) => {
const router = useRouter();
const slug = data?.post?.slug;
const {
data: { post, morePosts },
} = usePreviewSubscription(postQuery, {
params: { slug },
initialData: data,
enabled: preview && slug,
});
return (
<Layout>
<motion.article className='blog-article' initial='initial' animate='in' exit='out' variants={pageVariants} transition={pageTransition}>
<Scrollbar style={{ width: '100%', height: '100%' }}>
<figure className='hero-container'>
<h1 className='blog-title'>{post.title} </h1>
{post.mainImage && <img className='blog-hero' alt='Some alt Text' src={urlForImage(post.mainImage).url()} />}
</figure>
<div className='copy-block'>
<BlockContent blocks={post.body} imageOptions={{ w: 860, fit: 'max' }} {...sanityClient.config()} />
</div>
</Scrollbar>
</motion.article>
</Layout>
);
};
export async function getStaticProps({ params, preview = false }) {
const { post, morePosts } = await getClient(preview).fetch(postQuery, {
slug: params.slug,
});
return {
props: {
preview,
data: {
post,
morePosts: overlayDrafts(morePosts),
},
},
};
}
export async function getStaticPaths() {
const paths = await sanityClient.fetch(postSlugsQuery);
return {
paths: paths.map((slug) => ({ params: { slug } })),
fallback: true,
};
}
export default Post;
<figure className='hero-container'>
<h1 className='blog-title'>{post?.title} </h1>
{post?.mainImage && <img className='blog-hero' alt='Some alt Text' src={urlForImage(post?.mainImage).url()} />}
</figure>
During the build time next not aware of the post object and its key so it's better to make an optional chaining.
There are 2 things may want to add a in static generated page so it knows what to do if there isn't any props.
Add props checking in page, so it will not fail build.
if (!props) return null // or !data in your case.
Add notFound return props in getStaticProps so it knows how to handle 404 properly.
return {
notFound: true,
revalidate: 300,
}
Related
I'm trying to figure out how to set up a nextjs index.tsx page, that renders a page if the user is authenticated and another component if the user is not authenticated.
I can have the not authenticated component rendered properly, but I cannot have the authenticated page rendered correctly. I cant find a tutorial to explain how to put a page in the if statement so that the main nextjs index.tsx page renders the page I specify if there is an authenticated user.
I have an index.tsx in pages with:
import * as React from "react"
import { Box, Center, Spinner, VStack } from "#chakra-ui/react"
import Head from "next/head"
// import NextLink from "next/link"
import { useMe } from "lib/hooks/useMe"
import { DashLayout } from "components/DashLayout"
import { AuthedHomeLayout } from "components/AuthedHomeLayout"
import LandingPage from "components/landing/lp"
import { HomeLayout } from "components/HomeLayout"
export default function Home() {
const { me, loading } = useMe()
if (loading)
return (
<Center>
<Spinner />
</Center>
)
return (
<Box>
<Head>
<title>test</title>
</Head>
<Center flexDir="column" w="100%">
<VStack>
{me? <AuthedHomeLayout><DashLayout /></AuthedHomeLayout> : (
<HomeLayout><LandingPage /></HomeLayout>
)}
</VStack>
</Center>
</Box>
)
}
When I try this as an authenticated user, the DashLayout does load, but the links in it do not render.
The DashLayout has a set of links in it that form the pages of the dashboard:
import * as React from "react"
import { Box, Flex, Heading, Link, LinkProps, Stack, useColorModeValue } from "#chakra-ui/react"
import NextLink from "next/link"
import { useRouter } from "next/router"
const DashLayout: React.FC = ({ children }) => {
return (
<Box pt={10} pb={20} w="100%">
<Flex flexWrap={{ base: "wrap", md: "unset" }}>
<Box pos="relative">
<Stack
position="sticky"
top="100px"
minW={{ base: "unset", md: "200px" }}
mr={8}
flexDir={{ base: "row", md: "column" }}
mb={{ base: 8, md: 0 }}
spacing={{ base: 0, md: 4 }}
>
<ProfileLink href="/dash">Dashboard</ProfileLink>
<ProfileLink href="/dash/library">Library</ProfileLink>
<ProfileLink href="/dash/help">Help</ProfileLink>
</Stack>
</Box>
<Box w="100%">{children}</Box>
</Flex>
</Box>
)
}
export default DashLayout
interface ProfileLinkProps extends LinkProps {
href: string
}
const ProfileLink: React.FC<ProfileLinkProps> = ({ href, ...props }) => {
const { asPath } = useRouter()
const isActive = asPath === href
const activeColor = useColorModeValue("black", "white")
const inactiveColor = useColorModeValue("gray.600", "gray.500")
return (
<NextLink href={href} passHref>
<Link
pr={4}
h="25px"
justifyContent={{ base: "center", md: "flex-start" }}
textDecoration="none !important"
color={isActive ? activeColor : inactiveColor}
_hover={{ color: useColorModeValue("black", "white") }}
fontWeight={isActive ? "semibold" : "normal"}
>
{props.children}
</Link>
</NextLink>
)
}
The page I want to render if there is an auth user, is:
import * as React from "react"
import { gql } from "#apollo/client"
import { Center, Spinner, Stack, Text } from "#chakra-ui/react"
import { useUpdateMeMutation } from "lib/graphql"
import { useForm } from "lib/hooks/useForm"
import { useMe } from "lib/hooks/useMe"
import { useMutationHandler } from "lib/hooks/useMutationHandler"
import { UPLOAD_PATHS } from "lib/uploadPaths"
import Yup from "lib/yup"
import { ButtonGroup } from "components/ButtonGroup"
import { Form } from "components/Form"
import { withAuth } from "components/hoc/withAuth"
import { AuthedHomeLayout } from "components/AuthedHomeLayout"
import { ImageUploader } from "components/ImageUploader"
import { Input } from "components/Input"
import { DashLayout } from "components/DashLayout"
const _ = gql`
mutation UpdateMe($data: UpdateUserInput!) {
updateMe(data: $data) {
...Me
}
}
`
const ProfileSchema = Yup.object().shape({
email: Yup.string().email().required("Required").nullIfEmpty(),
firstName: Yup.string().required("Required").nullIfEmpty(),
lastName: Yup.string().required("Required").nullIfEmpty(),
})
function Dash() {
const { me, loading } = useMe()
const handler = useMutationHandler()
const [updateUser] = useUpdateMeMutation()
const updateAvatar = (avatar: string | null) => {
return handler(() => updateUser({ variables: { data: { avatar } } }), {
onSuccess: (_, toast) => toast({ description: "Avatar updated." }),
})
}
const defaultValues = {
email: me?.email || "",
firstName: me?.firstName || "",
lastName: me?.lastName || "",
}
const form = useForm({ defaultValues, schema: ProfileSchema })
const handleUpdate = (data: typeof defaultValues) => {
return form.handler(() => updateUser({ variables: { data } }), {
onSuccess: (_, toast) => {
toast({ description: "Info updated!" })
form.reset(data)
},
})
}
if (loading)
return (
<Center>
<Spinner />
</Center>
)
if (!me) return null
return (
<Stack spacing={6}>
<Tile>
<Text>alskjf</Text>
</Tile>
</Stack>
)
}
Dash.getLayout = (page: React.ReactNode) => (
<AuthedHomeLayout>
<DashLayout>{page}</DashLayout>
</AuthedHomeLayout>
)
export default withAuth(Dash)
I also tried defining the index.tsx condition as:
{me?
<Dash /> // Dash is defined as a page in the pages folder at dash/index
///<AuthedHomeLayout><DashLayout /></AuthedHomeLayout>
: (
<HomeLayout><LandingPage /></HomeLayout>
)}
How can I have index.tsx defined to render one page if there is an authed user and another if there is not?
I saw this post and tried using one of the suggestions it makes, as follows:
import Router from 'next/router';
{me? Router.push('/dash') : (
<HomeLayout><LandingPage /></HomeLayout>
)}
When I try this, I get errors that read:
[{
"resource": "/src/pages/index.tsx",
"owner": "typescript",
"code": "2322",
"severity": 8,
"message": "Type 'Element | Promise<boolean>' is not assignable to type 'ReactNode'.\n Type 'Promise<boolean>' is not assignable to type 'ReactNode'.",
"source": "ts",
"startLineNumber": 32,
"startColumn": 13,
"endLineNumber": 34,
"endColumn": 15,
"relatedInformation": [
{
"startLineNumber": 1360,
"startColumn": 9,
"endLineNumber": 1360,
"endColumn": 17,
"message": "The expected type comes from property 'children' which is declared here on type 'IntrinsicAttributes & OmitCommonProps<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof StackProps> & StackProps & { ...; }'",
"resource": "repo/node_modules/#types/react/index.d.ts"
}
]
}]
In the solutions you tried, the last one was almost correct.
You were on the right path, that you should redirect the user to the /dash page if he is authenticated. But you were doing the redirection in the return statement of your component, which is not where you want to do any side effect logic.
Your attempt:
import Router from 'next/router';
{me? Router.push('/dash') : (
<HomeLayout><LandingPage /></HomeLayout>
)}
will not work because Router.push returns a <Promise<boolean>>.
Don't forget that React components must return React elements. In your case when the user is authenticated, you are returning a promise not a React element.
So your redirection (which is a side effect) should be done inside a useEffect hook.
In order to fix this, Next documentation provides a clear example of how to do it correctly. What you are looking for is the last code block of this section (the one just before this section).
Don't forget to use a valid router instance via the useRouter hook provided by next/router.
So your code now becomes something like:
import { useEffect } from 'react';
import { useRouter } from 'next/router';
// Whatever Component you were doing the redirect
const export YourComponent = () => {
// your component hooks and states
const { me, loading } = useMe();
const router = useRouter();
// Here is what you were missing
useEffect(() => {
if (me) {
router.push('/dash');
}
}, [me]);
// you can add a loader like you did before
return loading ? (
<Center><Spinner /></Center>
) : (
<HomeLayout><LandingPage /></HomeLayout>
);
};
It should be enough to get to what you're looking for.
As a side note, your first solution:
{me?
<Dash /> // Dash is defined as a page in the pages folder at dash/index
///<AuthedHomeLayout><DashLayout /></AuthedHomeLayout>
: (
<HomeLayout><LandingPage /></HomeLayout>
)}
cannot work, as <Dash /> is a Next Page which is associated with a route based on its file name. You can look at it like an entry point.
I have another question in the project I'm doing, using NextJS to create a site with a blog part in Wordpress, through WPGraphQL, and I need support in a specific part. Let's go...
I managed to pull the highlighted image with almost no problems, I broke my head a bit, but it worked. The result that's functioning is the post excerpt, the code and the query were as follows (in time: image merely to test, it was the first idea that came to my mind, the site is not about Pokemon):
Image with working image in the excerpt, codes below
NextJS code:
import { LastPosts, PostContainer } from "./Styled";
const RecentBlogPosts = ({lastPosts}) => {
const posts = lastPosts;
return (
<LastPosts>
<h1> ÚLTIMAS POSTAGENS </h1>
{posts?.map((post) => {
return (
<PostContainer key={post.slug}>
<img src={post.featuredImage?.node.sourceUrl} alt="" />
<Link href={`/post/${post.slug}`}>
<a>
<h3> { post.title } </h3>
<div dangerouslySetInnerHTML={{ __html: post.excerpt }} />
<button> Saiba mais </button>
</a>
</Link>
</PostContainer>
)
})}
</LastPosts>
)
};
export default RecentBlogPosts;
Query for this part:
export const RECENT_POSTS = `query RecentPosts {
posts(where: {orderby: {field: DATE, order: DESC}}, first: 2) {
nodes {
id
slug
title
excerpt
featuredImage {
node {
sourceUrl
}
}
}
}
}`;
But I tried to pull the same image in the full blogpsot and it wasn't working... It appears when I view the post from the generic WordPress admin template, but not at my NextJS site, which i See through localhost:3000/post/[here would be the post title in slug] that I'm using. The rest is normal, text with all fonts and specifications with styled components, as well as tags, they work without any problem. The following is the same schema: image with result, code and query that I am using, this time for the part where I'm having problems:
Image with blogpost not working, codes below
NextJS code:
import fetcher from "../../lib/fetcher";
import { GET_ALL_POSTS_WITH_SLUG, POST_BY_SLUG } from "../../lib/wordpress/api";
import { useRouter } from "next/router";
import { Reset } from "../../constants/StyledConstants";
import Header from "../../components/Header/Header";
import { BlogArticle, BlogPostContent, TagLinks, TagWrapper } from "./StyledPost";
import Footer from "../../components/Footer/Footer";
const post = ({ postData }) => {
const blogPost = postData.data.post;
console.log(postData);
const tags = postData.data.post.tags.nodes;
const router = useRouter;
if(!router.isFallback && !blogPost?.slug) {
return <div>erro</div>
}
return (
<>
<Reset />
<Header />
<BlogPostContent>
{router.isFallback ? (
<div> Carregando...... </div>
) : (
<div>
<h1> { blogPost.title } </h1>
<img src={post.featuredImage?.node.sourceUrl} alt="imagem não aparece" />
<BlogArticle dangerouslySetInnerHTML={{ __html: blogPost.content }} />
<TagWrapper>
{tags.map((tag) => <TagLinks href={`/tags/${tag.slug}`} key={tag.slug}> { tag.name } </TagLinks>)}
</TagWrapper>
</div>
)}
</BlogPostContent>
<Footer />
</>
)
}
export default post;
export async function getStaticPaths() {
const response = await fetcher(GET_ALL_POSTS_WITH_SLUG);
const allposts = await response.data.posts.nodes;
return {
paths: allposts.map((post) => `/post/${post.slug}`) || [],
fallback: false
};
}
export async function getStaticProps({ params }) {
const variables = {
id: params.slug,
idType: "SLUG"
};
const data = await fetcher(POST_BY_SLUG, { variables })
return {
props: {
postData: data
},
};
}
Query being used:
export const POST_BY_SLUG = `query PostBySlug($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
title
slug
date
content
featuredImage {
node {
sourceUrl
}
}
tags {
nodes {
name
slug
}
}
}
}`;
I tried to use {post.featuredImage?.node.sourceUrl} because, as far as I understand, following the same basis I did for the excerpt in the blogspot, it should work, but I guess I was wrong... I tried to think of other ways to do it to get to the image, without success... Could someone help to point out where I am wrong please? Thank you very much in advance!!
I am working with react to fetch data from the node backend and implement the UI with the data. I rendered the UI conditionally but I do get an error in the console saying that nothing was returned from render. Here is my code
import React, { useEffect, useState } from "react";
import OT from "#opentok/client";
import { OTSession, OTPublisher, OTStreams, getPublisher } from "opentok-react";
import Connection from "./Connection";
import Publisher from "./Publisher";
import Subscriber from "./Subscriber";
import { useParams } from "react-router-dom";
import { connect } from "react-redux";
import { Creators } from "../../../services/redux/event/actions";
import { PropTypes } from "prop-types";
import { makeStyles, Container } from "#material-ui/core";
function Host(props) {
const [connect, setConnect] = useState(false);
const params = useParams();
const { event, error, isCreatingEvent } = props;
console.log(event, "event");
const handleSessionOn = () => {
setConnect(true);
};
useEffect(() => {
props.getSingle(params.id);
}, []);
if (isCreatingEvent) {
return <div>Loading .....</div>;
}
if (error) {
return <div>Error: {error.error_message}</div>;
}
if (event.sessionId != undefined) {
const { API_KEY: apiKey, sessionId, token } = event;
console.log(apiKey, sessionId, token)
return (
<div style={{ zIndex: 100 }}>
<Connection connect={connect} />
<h3 style={{ color: "red" }}>This is apiKey connect</h3>
<OTSession
sessionId={sessionId}
token={token}
apiKey={apiKey}
onConnect={handleSessionOn}
>
<Publisher />
<OTStreams>
<Subscriber sessionId={sessionId} />
</OTStreams>
</OTSession>
</div>
)
}
}
Host.protoTypes = {
event: PropTypes.object.isRequired,
error: PropTypes.string,
};
const mapDispatchToProps = (dispatch) => {
return {
getSingle: (id) => {
dispatch(Creators.getOneEvent(id));
},
};
};
const mapStateToProps = (state) => (
console.log(state),
{
event: state.event.event,
error: state.event.error,
isCreatingEvent: state.event.isCreatingEvent,
}
);
export default connect(mapStateToProps, mapDispatchToProps)(Host);
Can anyone please help me out? I used the redux state to connect with Vonage API but the OTSession is not being rendered.
You called the return function only on the if statement.
You should call the return function on the else statement.
Like this.
const { API_KEY: apiKey, sessionId, token } = event;
{event.sessionId != undefined ? (
<div style={{ zIndex: 100 }}>
<Connection connect={connect} />
<h3 style={{ color: "red" }}>This is apiKey connect</h3>
<OTSession
sessionId={sessionId}
token={token}
apiKey={apiKey}
onConnect={handleSessionOn}
>
<Publisher />
<OTStreams>
<Subscriber sessionId={sessionId} />
</OTStreams>
</OTSession>
</div>
) : null}
I'm creating the redux state in this page :
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import wrapper from '../redux/store';
import Container from '../components/Container/Container';
import Card from '../components/Card/Card';
import Circle from '../components/Circle/Circle';
import PieChart from '../components/PieChart/PieChart';
import Accordion from '../components/Accordion/Accordion';
import RadioButton from '../components/Ui/RadioButton/RadioButton';
import { manageList, reportList } from '../components/helper';
import { getManageListAndCategoryId } from '../redux/actions/actions';
const Panel = ({ manageProductsList }) => (
<>
{console.log(manageProductsList)}
<MainContainer>
<Title>Управление</Title>
<ContainersWrapper>
{manageProductsList.map((item, index) => <Card key={index} title={item.title} type="service" serviceName={item.value} />)}
</ContainersWrapper>
<SecondSection>
<CustomContainer>
<Title>Отчетность</Title>
<p>Показатели за:</p>
Здесь будут ТАБЫ
<ContainersWrapper>
{reportList.map((item, index) => <Card key={index} item={item} type="report" />)}
</ContainersWrapper>
<DiagreammWrapper>
<PieChart />
<Circle percent={20} />
<Circle percent={87} />
<Circle percent={30} />
<Circle percent={47} />
</DiagreammWrapper>
</CustomContainer>
</SecondSection>
<CustomContainer>
<TitleTwo>Доступные отчеты</TitleTwo>
<Accordion />
<RadioButton />
</CustomContainer>
</MainContainer>
</>
);
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getManageListAndCategoryId(manageList));
});
const mapStateToProps = (state) => ({
manageProductsList: state.mainReducer.manageProductsList,
});
export default connect(mapStateToProps, null)(Panel);
And I still can see the data manageProductsList (screenshot) in Redux in this page. But when I navigate to another dynamic route page forms/[id.tsx]
import React from 'react';
import { connect } from 'react-redux';
import wrapper from '../redux/store';
import { util, manageList, reportList } from '../../components/helper';
import { getManageListAndCategoryId } from '../../redux/actions/actions';
export async function getStaticPaths(categoryIds) {
console.log('categoryIds', categoryIds);
//temporarely make static path data while categoryIds is undefined
const paths = [
{ params: { id: 'object' } },
{ params: { id: 'service' } },
{ params: { id: 'club_cards' } },
{ params: { id: 'schedule' } },
{ params: { id: 'agents' } },
{ params: { id: 'abonements' } },
{ params: { id: 'price_category' } },
{ params: { id: 'person_data' } },
{ params: { id: 'roles' } },
];
return {
paths,
fallback: false,
};
}
export async function getStaticProps({ params, manageProductsList }) {
// const postData = util.findFormData(params.id, manageProductsList);
const postData = { title: 'asdsadasdsad' };
return {
props: {
postData,
},
};
}
const Form = ({ manageProductsList }) => (
<div>
{console.log(manageProductsList)}
{/* {postData.title} */}
dasdsadsad
</div>
);
const mapStateToProps = (state) => ({
categoryIds: state.mainReducer.categoryIds,
manageProductsList: state.mainReducer.manageProductsList,
});
export default connect(mapStateToProps, null)(Form);
the manageProductsList and categoryIds are empty arrays (screenshot 2)
I am using native Link from next/link component to navigate the page
Here is Card component which navigate to dynamic page:
import React, { FunctionComponent, HTMLAttributes } from 'react';
import styled from 'styled-components';
import Link from 'next/link';
import EditIcon from '#material-ui/icons/Edit';
import AddIcon from '#material-ui/icons/Add';
interface CardProps extends HTMLAttributes<HTMLOrSVGElement>{
title: string
type: string
item?: {
title: string
amount: number
}
serviceName: string
}
const Card: FunctionComponent<CardProps> = ({
type, title, serviceName, item,
}) => (
<>
{
type === 'service'
&& (
<FirstSection>
<h1>{title}</h1>
<ImageWrapper>
<Link href={`/forms/${serviceName}`}>
<a><AddIcon fontSize="large" onClick={(e) => { console.log(serviceName); }} /></a>
</Link>
<EditIcon />
</ImageWrapper>
</FirstSection>
)
}
{
type === 'report'
&& (
<SecondSection>
<h1>{item.title}</h1>
<p>{item.amount}</p>
</SecondSection>
)
}
</>
);
export default Card;
I would be very gratefull if someone can help
Your <Link> will cause server-side rendering, you can observe whether the browser tab is loading or not when navigate to another page. If it is, the page will reload and the redux state would be refresh.
The official docs shows the right way for using dynamic route.
<Link href="/forms/[id]" as={`/forms/${serviceName}`}>
I am trying to implement a loading screen when changing routes in my Next.js app, for example /home -> /about.
My current implementation is as follows. I am setting the initial loaded state to false and then changing it on componentDidMount. I am also calling the Router.events.on function inside componentDidMount to change the loading state when the route change starts.
_app.js in pages folder
class MyApp extends App {
constructor(props) {
super(props);
this.state = {
loaded: false,
};
}
componentDidMount() {
this.setState({ loaded: true });
Router.events.on('routeChangeStart', () => this.setState({ loaded: false }));
Router.events.on('routeChangeComplete', () => this.setState({ loaded: true }));
}
render() {
const { Component, pageProps } = this.props;
const { loaded } = this.state;
const visibleStyle = {
display: '',
transition: 'display 3s',
};
const inVisibleStyle = {
display: 'none',
transition: 'display 3s',
};
return (
<Container>
<>
<span style={loaded ? inVisibleStyle : visibleStyle}>
<Loader />
</span>
<span style={loaded ? visibleStyle : inVisibleStyle}>
<Component {...pageProps} />
</span>
</>
</Container>
);
}
}
This works perfectly fine but I feel like there may be a better solution more elegant solution. Is this the only way which isn't cumbersome to implement this loading feature or is there an alternative ?
Using the new hook api,
this is how I would do it..
function Loading() {
const router = useRouter();
const [loading, setLoading] = useState(false);
useEffect(() => {
const handleStart = (url) => (url !== router.asPath) && setLoading(true);
const handleComplete = (url) => (url === router.asPath) && setLoading(false);
router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeComplete', handleComplete)
router.events.on('routeChangeError', handleComplete)
return () => {
router.events.off('routeChangeStart', handleStart)
router.events.off('routeChangeComplete', handleComplete)
router.events.off('routeChangeError', handleComplete)
}
})
return loading && (<div>Loading....{/*I have an animation here*/}</div>);
}
Now <Loading/> is going to show up whenever the route will change...
I animate this using react-spring, but you can use any library you prefer to do this.
You can even take a step further and modify when the component shows up by modifying the handleStart and handleComplete methods that gets a url.
Why not use nprogress as follows in _app.js
import React from 'react';
import Router from 'next/router';
import App, { Container } from 'next/app';
import NProgress from 'nprogress';
NProgress.configure({ showSpinner: publicRuntimeConfig.NProgressShowSpinner });
Router.onRouteChangeStart = () => {
// console.log('onRouteChangeStart triggered');
NProgress.start();
};
Router.onRouteChangeComplete = () => {
// console.log('onRouteChangeComplete triggered');
NProgress.done();
};
Router.onRouteChangeError = () => {
// console.log('onRouteChangeError triggered');
NProgress.done();
};
export default class MyApp extends App { ... }
Link to nprogress.
You also need to include style file as well. If you put the css file in static directory, then you can access the style as follows:
<link rel="stylesheet" type="text/css" href="/static/css/nprogress.css" />
Make sure the CSS is available in all pages...
It will work for all your routes changing.
For anyone coming across this in 2021, the package nextjs-progressbar makes this super easy. In your Next.js _app.js, simply add:
import NextNProgress from 'nextjs-progressbar';
export default function MyApp({ Component, pageProps }) {
return (
<>
<NextNProgress />
<Component {...pageProps} />;
</>
);
}
And done!
Demo and screenshot:
New Update with NProgress:
import Router from 'next/router'
import Link from 'next/link'
import Head from 'next/head'
import NProgress from 'nprogress'
Router.events.on('routeChangeStart', (url) => {
console.log(`Loading: ${url}`)
NProgress.start()
})
Router.events.on('routeChangeComplete', () => NProgress.done())
Router.events.on('routeChangeError', () => NProgress.done())
export default function App({ Component, pageProps }) {
return (
<>
<Head>
{/* Import CSS for nprogress */}
<link rel="stylesheet" type="text/css" href="/nprogress.css" />
</Head>
<Component {...pageProps} />
</>
)
}
If you use Tailwind CSS, copy the code from here: https://unpkg.com/nprogress#0.2.0/nprogress.css and paste the code into your global CSS file.
if you want to disable the spinner add the below code in your _app.tsx/jsx file and remove the spinner styles from CSS.
NProgress.configure({ showSpinner: false });
Source Links:
https://github.com/rstacruz/nprogress
https://nextjs.org/docs/api-reference/next/router
Progress bar like NProgress in 90 lines of code (vs NProgress v0.2.0 is 470 lines .js + 70 lines .css):
import { useEffect, useReducer, useRef } from 'react';
import { assert } from './assert';
import { wait } from './wait';
import { getRandomInt } from './getRandomNumber';
let waitController: AbortController | undefined;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function useProgressBar({
trickleMaxWidth = 94,
trickleIncrementMin = 1,
trickleIncrementMax = 5,
dropMinSpeed = 50,
dropMaxSpeed = 150,
transitionSpeed = 600
} = {}) {
// https://stackoverflow.com/a/66436476
const [, forceUpdate] = useReducer(x => x + 1, 0);
// https://github.com/facebook/react/issues/14010#issuecomment-433788147
const widthRef = useRef(0);
function setWidth(value: number) {
widthRef.current = value;
forceUpdate();
}
async function trickle() {
if (widthRef.current < trickleMaxWidth) {
const inc =
widthRef.current +
getRandomInt(trickleIncrementMin, trickleIncrementMax); // ~3
setWidth(inc);
try {
await wait(getRandomInt(dropMinSpeed, dropMaxSpeed) /* ~100 ms */, {
signal: waitController!.signal
});
await trickle();
} catch {
// Current loop aborted: a new route has been started
}
}
}
async function start() {
// Abort current loops if any: a new route has been started
waitController?.abort();
waitController = new AbortController();
// Force the show the JSX
setWidth(1);
await wait(0);
await trickle();
}
async function complete() {
assert(
waitController !== undefined,
'Make sure start() is called before calling complete()'
);
setWidth(100);
try {
await wait(transitionSpeed, { signal: waitController.signal });
setWidth(0);
} catch {
// Current loop aborted: a new route has been started
}
}
function reset() {
// Abort current loops if any
waitController?.abort();
setWidth(0);
}
useEffect(() => {
return () => {
// Abort current loops if any
waitController?.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
start,
complete,
reset,
width: widthRef.current
};
}
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useProgressBar } from './useProgressBar';
const transitionSpeed = 600;
// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function RouterProgressBar(
props?: Parameters<typeof useProgressBar>[0]
) {
const { events } = useRouter();
const { width, start, complete, reset } = useProgressBar({
transitionSpeed,
...props
});
useEffect(() => {
events.on('routeChangeStart', start);
events.on('routeChangeComplete', complete);
events.on('routeChangeError', reset); // Typical case: "Route Cancelled"
return () => {
events.off('routeChangeStart', start);
events.off('routeChangeComplete', complete);
events.off('routeChangeError', reset);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return width > 0 ? (
// Use Bootstrap, Material UI, Tailwind CSS... to style the progress bar
<div
className="progress fixed-top bg-transparent rounded-0"
style={{
height: 3, // GitHub turbo-progress-bar height is 3px
zIndex: 1091 // $zindex-toast + 1 => always visible
}}
>
<div
className="progress-bar"
style={{
width: `${width}%`,
//transition: 'none',
transition: `width ${width > 1 ? transitionSpeed : 0}ms ease`
}}
/>
</div>
) : null;
}
How to use:
// pages/_app.tsx
import { AppProps } from 'next/app';
import Head from 'next/head';
import { RouterProgressBar } from './RouterProgressBar';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>My title</title>
<meta name="description" content="My description" />
</Head>
<RouterProgressBar />
<Component {...pageProps} />
</>
);
}
More here: https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047