I am using vercel for NextJS and this is my setup in getStaticPaths
const paths = posts.map((post) => ({
params: { player: post.player, id: post.id },
}))
return { paths, fallback: true }
When I set the fallback to true, I have got this error in vercel:
21:55:01.736 info - Generating static pages (1752/1752)
21:55:01.736 > Build error occurred 21:55:01.739 Error: Export
encountered errors on following paths: 21:55:01.739
/clip/[player]/[id]
It is ok when fallback is set to false but I really like to set fallback set to true so that pages can be updated frequently. Any help will be greatly appreciated...
Inside your /clip/[player]/[id].js file, you need to handle the fallback state when that page is being requested on-demand.
// pages/posts/[id].js
import { useRouter } from 'next/router'
function Post({ post }) {
const router = useRouter()
// If the page is not yet generated, this will be displayed
// initially until getStaticProps() finishes running
if (router.isFallback) {
return <div>Loading...</div>
}
// Render post...
}
// This function gets called at build time
export async function getStaticPaths() {
return {
// Only `/posts/1` and `/posts/2` are generated at build time
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
// Enable statically generating additional pages
// For example: `/posts/3`
fallback: true,
}
}
// This also gets called at build time
export async function getStaticProps({ params }) {
// params contains the post `id`.
// If the route is like /posts/1, then params.id is 1
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()
// Pass post data to the page via props
return {
props: { post },
// Re-generate the post at most once per second
// if a request comes in
revalidate: 1,
}
}
export default Post
https://nextjs.org/docs/basic-features/data-fetching#fallback-true
What I did was conditionally render my component. So, my component receives the object data and if I need to use a value from data, such as "title", I will do...
data?.title
Also, for my entire return component I will conditionally render it. For example...
{data !== undefined ? (
<div className ='main-content'>
<p> This is the content that I want rendered if data is defined </p>
</div>
) : (
<div className = 'fallback-content'>
<p> This shows if data == undefined </p>
</div>
)
Related
I am trying to use vuex-persist on nuxt.js but when try to read the data in a v-if doesn't work.
I created a new file inside plugins called vuex-persis.js with the following code:
import VuexPersistence from 'vuex-persist'
export default ({ store }) => {
new VuexPersistence({
storage: window.localStorage,
}).plugin(store);
}
and into nuxt.config.js I added the following code:
plugins: [{ src: '~/plugins/vuex-persist', mode: 'client' }],
The code that I use in order to store and get data is the following:
this.$store.commit('login', { idToken });
store.state.isLogin
and I have added the following file inside the project ./store/index.js
export const state = () => ({
isLogin: false,
idToken: '',
});
export const mutations = {
login(state, newState) {
state.isLogin = true;
state.idToken = newState.idToken;
},
logout(state) {
state.isLogin = false;
state.idToken = '';
},
};
I have the following navbar when I try to get the data from store:
navbar-button(to='/board', v-if='$store.state.isLogin')
p {{ $t("navbar_board") }}
navbar-button(v-if='$store.state.isLogin', #click.native='logout')
p {{ $t("navbar_logout") }}
but always enter in the same place, log in.
I tried to use a computed like this but doesn't work on the v-if
computed: {
isLogin: function () {
console.log(this.$store.state.isLogin);
return this.$store.state.isLogin;
},
},
the console.log prints the value stored in the browser but the console.log in node prints always the default state.
Anyway, the console.log always prints the correct value stored in the localstorage.
Any idea about how can I use that data in the v-if?
Thanks
Finally solved directly using $store.state.isLogin in the v-if
When navigating from a link in the same web app to the dynamically routed page clicking on a link the result is as intended: I navigate to the page for a product (http://localhost/1).
But when I directly navigate by naming the product number specifically in the search bar (navigating to http://localhost/2), I get the following error:
Server Error
TypeError: Cannot read property 'image' of undefined
> | <Image src={"/../public/images/" + p.image}
^
So far I've tried making the types match and reading the Next JS docs on dynamically routing.
I've removed the array zero from the filter but still no resolution.
Could it be possible that the routing only works when clicking on a link in Next JS? Is there some missing setting I've neglected?
pages/[pid].js
import { useRouter } from 'next/router'
import Image from 'next/image'
import data from '../products.json'
export default function Template() {
const router = useRouter()
const { pid } = router.query
const p = data.filter(product => product._id == pid)[0] // Choose one result
return (
<Image src={"/../public/images/" + p.image}
height="500px"
width="500px" />
)
}
products.json
[
{
"_id": 1,
"name": "Toyota",
"image": "toyota.png"
},
{
"_id": 2,
"name": "BMW",
"image": "bmw.png"
}
]
Update: I've tried to hardcode the src attribute in the Image tag and the new error says the other references are the issue. So I can safely say the issue is to do with no object returned when the data object is called.
I solved the issue!
It was not enough to use Dynamic Routes by using the 'useRouter()' function. I also had to define these two functions:
export async function getStaticProps(context) {
// No-op since getStaticPaths needs getStaticProps to be called.
return { props: {} }
}
export async function getStaticPaths() {
const dynamicFiles = products.map(product => (
{
params: { pid: String(product._id) }, // Product IDs are ints in JSON file
}
))
return {
paths: dynamicFiles,
fallback: false
}
}
This makes sense since you wouldn't want random paths to be used as a variable. For example, then a user would be able to specify http://localhost/1234 when 1234 is not a valid option.
https://nextjs.org/learn/basics/dynamic-routes/implement-getstaticprops
I have a [slug].js page that will fetch API to get the destination page
export async function getServerSideProps({ query, res }) {
const slug = query.slug;
try {
const destination = await RoutingAPI.matchSlug(slug);
res.writeHead(302, { Location: destination });
res.end();
// return {
// redirect: {
// permanent: true,
// destination,
// },
// }
} catch (error) {
return {
notFound: true
}
}
}
If I client redirect from another page to slug page, it works and keeps URL the same as slug but it makes the browser reload. If I use
return {
redirect: {
permanent: true,
destination,
},
}
it will not reload the browser but it change URL to the destination, not the same as slug. How do i fix this problem? I would appreciate any ideas, thanks
You can use rewrites to achieve this. From the docs:
Rewrites allow you to map an incoming request path to a different destination path.
In your next.config.js add this:
module.exports = {
async rewrites() {
return [
{
source: "/:slug",
destination: "/your-destination",
},
];
},
};
rewrites is an async function that expects an array to be returned holding objects with source and destination properties:
source is the incoming request path pattern.
destination is the path you want to route to.
Ok, I have a solution, it doesn't reload and it doesn't change the url.. however it requires some client side script.. as you see in the example below
Hope you find it to your liking :D
Here's my codesandbox
index.js(from your codesandbox)
import Link from "next/link";
export default function IndexPage() {
return (
<div>
Hello World.{" "}
<Link href="/something-slug">
<a id="myElem">Go to slug pageee</a>
</Link>
<script
dangerouslySetInnerHTML={{
__html: `
let elem=document.getElementById('myElem')
elem.addEventListener('click',async function(event){
event.preventDefault()
console.log(event.path[0].href)
let myFetch=await fetch(event.path[0].href)
let text=await myFetch.text()
let newHTML=document.createElement('html')
newHTML.innerHTML=text
let _next=document.getElementById('__next')
_next.innerHTML=""
let requestedNext=[...newHTML.getElementsByTagName('div')].filter(a=>a.id=="__next")[0]
let scripts=[...requestedNext.getElementsByTagName('script')]
scripts.forEach(a=>eval(a.innerHTML)) //eval any scripts sent by YOUR requested _next
console.log(requestedNext)
_next.innerHTML=requestedNext.innerHTML
})
`
}}
></script>
</div>
);
}
slug.js(again, from your codesandbox)
export default function AboutPage() {
return <div>About us</div>;
}
export async function getServerSideProps({ query, req, res }) {
return {};
}
I'm starting with Next.js and after going through docs, I cannot figure out how to get the route param code inside getStaticPaths method as shown below!?. code is not known before hand by any means and it can be anything.
I don't want to call api and get the data using useEffect inside the component.
File: pages/post/[code].js
import React from 'react';
import apiCall from 'api/something';
export default ({post}) => {
return <>
render components here based on prop `post`
</>
}
export async function getStaticPaths() {
// How to get [code] from the route here, which can be used below?
return {
paths: // NEED [code] HERE from current route,
fallback: false
}
}
export async function getStaticProps(ctx) {
return {
props: {
// [ctx.code] resolved from current route with the help of getStaticPaths,
post: apiCall(ctx.code)
}
}
}
I've tried getServerSideProps which works for me:
export const getServerSideProps = async (ctx) => {
return {
props: {
post: await apiCall(ctx.query.code)
}
};
};
But it fails when I do next export stating:
pages with getServerSideProps can not be exported. See more info here: https://err.sh/next.js/gssp-export
After investigating further on this error I found this solution, which is not feasible for me as my app is hosted on Heroku.
I'm trying to server-side render the html along with the data based on the route param code. But not able to do so now.
The purpose of the function getStaticPaths is to generate a list of paths for which static HTML will be rendered at build time. For example, for a list of 10 posts, you can generate 10 posts/[id] routes ahead of time if you know the id of the posts.
How getStaticPaths works with dynamic routes in more details..
Suppose you have a dynamic route /posts/[postId] if you choose to use static-generation you have to generate a list of paths that will include the postId as a route param and for each path returned, the function getStaticProps will be called to query the data at build time. Example,
// for /post/[postId]
export const getStaticPaths = async () => {
// if you know all the postId ahead of time
const paths = [
{ params: { postId: '1234' } }, // keep in mind postId has to be a string
{ params: { postId: '3792' } },
{ params: { postId: '1749' } },
]
return {
paths,
fallback: false // we are disabling fallback because we know all the paths ahead of time
}
}
// for each path returned getStaticProps will be called at build time
export const getStaticProps = async (context) => {
// you have access to the postId params that you returns from
// getStaticPaths here
const postId = context.params.postId
// now you can query the data from postId and return as props
return {
props: // queried data
}
}
If fallback is set to false any for any route path that is not returned from the function getStaticPaths nextjs will simply show a 404 error page.
How to use fallback: true to generate static pages for route params not known ahead of time
If you know some postId of the posts and the data for the posts do not change very often, you can choose to generate the pages with fallback property set to true, which will display a fallback version of the page for the paths that are not returned from the function getStaticPaths. And on request for the page nextjs will call getStaticProps and send the data as JSON which will be used to render the page in the browser.
Example,
// for /post/[postId]
export const getStaticPaths = async () => {
// you can get how many ever postIds are know ahead of time
// and return as paths with fallback set to true
const posts = // queried data from db or fetched from remote API
const paths = posts.map(post => { params:{ postId: post.id.toString() }})
return {
paths,
fallback: true
}
}
// in your page Component check for fallback and render a loading indicator
import { useRouter } from 'next/router';
const MyPage = (props) => {
// before you do anything
const router = useRouter();
if (router.isFallback) {
return <div>Loading....</div>
}
// rest of your page logic
}
If your data is very dynamic, let's say changing every 30mins or an hour or so. You can choose to use server-side rendering which will fetch the data on per request basis, but TTFB(time to first byte) will be higher. For example,
// for /post/[postId]
export const getServerSideProps = async (context) => {
// you also have access to the param postId from the context
const postId = context.params.postId
// query the data based on the postId and return as props
return {
props: // queried data
}
}
Keep in mind if you choose to go with getServerSideProps the function will be called on per-request basis so time to first byte will be higher.
Depending on use-cases you can also use static generation with client-side data fetching using swr from nextjs team repo link.
As I understand, you want to statically generate dynamic routes at build time.
To do so you need to let Next.js know what pages to generate, by specifying all codes.
export async function getStaticPaths() {
// you don't need here a code from current route
// but you need to specify all known post codes
return {
paths: [
{ params: { code: '1' } },
{ params: { code: '2' } },
{ params: { code: '3' } },
]
fallback: false
}
}
You would need to re-build app every time you change the posts.
Use getServerSideProps if you don't want to re-build project every time. Then the data would be fetched at request time. You can't export it because it requires Node.js server.
I have the following sequence happening:
Main screen
Loading screen
Results screen
On homepage, when someone clicks a button, I send them to the loading screen, thru:
this.$router.push({path: "/loading"});
And once their task finishes, they are sent to the results screen via
this.$router.push({path: "/results/xxxx"});
The problem is, usually they want to go from results back to the main screen, but when they click back, they're sent to loading again which sends them back to results, so they're stuck in an infinite loop & unable to go back to main screen.
Any ideas how to fix this? I'd ideally like if there was an option like:
this.$router.push({path: "/loading", addToHistory: false});
which would send them to the route without adding it to history.
This should have a real answer using this.$router.replace:
// On login page
// Use 'push' to go to the loading page.
// This will add the login page to the history stack.
this.$router.push({path: "/loading"});
// Wait for tasks to finish
// Use 'replace' to go to the results page.
// This will not add '/loading' to the history stack.
this.$router.replace({path: "/results/xxxx"});
For further reading the Vue Router is using History.pushState() and History.replaceState() behind the scenes.
There is a perfect way to handle this situation
You can use in-component guard to control the route in granule level
Make the following changes in your code
In main screen component
Add this beofreRouteLeave guard in component options, before leaving to 'result screen' you are setting the route to go only
through loading screen
beforeRouteLeave(to, from, next) {
if (to.path == "/result") {
next('/loading')
}
next();
},
In loading screen component
If the route go backs from result to loading then , it should not land
here and directly jump to main screen
beforeRouteEnter(to, from, next) {
if (from.path == "/result") {
next('/main')
}
next();
},
In loading screen, The beforeRouteEnter guard does NOT have access to
this, because the guard is called before the navigation is confirmed,
thus the new entering component has not even been created yet. So taking the advantage of this, you won't get the infinite calls fired when routing from results screen
In result screen component
if you use go back then it should not land in loading and directly
jump to main screen
beforeRouteLeave(to, from, next) {
if (to.path == "/loading") {
next('/')
}
next();
},
I have just created small vue application to reproduce the same issue. It works in my local as per your question. Hope it resolves your issue as well.
I guess router.replace is the way to go - but still some lines of thought (untested):
Basically on $router change it renders the loading-component until it emits load:stop, then it renders the router-view
import { Vue, Component, Watch, Prop } from "vue-property-decorator";
#Component<RouteLoader>({
render(h){
const rv = (this.$slots.default || [])
.find(
child => child.componentOptions
//#ts-ignore
&& child.componentOptions.Ctor.extendedOptions.name === "RouterView"
)
if(rv === undefined)
throw new Error("RouterView is missing - add <router-view> to default slot")
const loader = (this.$slots.default || [])
.find(
child => child.componentOptions
//#ts-ignore
&& child.componentOptions.Ctor.extendedOptions.name === this.loader
)
if(loader === undefined)
throw new Error("LoaderView is missing - add <loader-view> to default slot")
const _vm = this
const loaderNode = loader.componentOptions && h(
loader.componentOptions.Ctor,
{
on: {
// "load:start": () => this.loading = true,
"load:stop": () => _vm.loading = false
},
props: loader.componentOptions.propsData,
//#ts-ignore
attrs: loader.data.attrs
}
)
return this.loading && loaderNode || rv
}
})
export default class RouteLoader extends Vue {
loading: boolean = false
#Prop({default: "LoaderView"}) readonly loader!: string
#Watch("$route")
loads(nRoute: string, oRoute: string){
this.loading = true
}
}
#Component<Loader>({
name: "LoaderView",
async mounted(){
await console.log("async call")
this.$emit("load:stop")
// this.$destroy()
}
})
export class Loader extends Vue {}
This is a tough call considering how little we know about what's occurring in your loading route.
But...
I've never had a need to build a loading route, only ever loading component(s) that gets rendered on multiple routes during init/data gathering stage.
One argument for not having a loading route would be that a user could potentially navigate directly to this URL (accidentally) and then it seems like it wouldn't have enough context to know where to send the user or what action to take. Though this could mean that it falls through to an error route at this point. Overall, not a great experience.
Another is that if you simplify your routes, navigation between routes becomes much simpler and behaves as expected/desired without the use of $router.replace.
I understand this doesn't solve the question in the way you're asking. But I'd suggest rethinking this loading route.
App
<shell>
<router-view></router-view>
</shell>
const routes = [
{ path: '/', component: Main },
{ path: '/results', component: Results }
]
const router = new VueRouter({
routes,
})
const app = new Vue({
router
}).$mount('#app')
Shell
<div>
<header>
<nav>...</nav>
</header>
<main>
<slot></slot>
</main>
</div>
Main Page
<section>
<form>...</form>
</section>
{
methods: {
onSubmit() {
// ...
this.$router.push('/results')
}
}
}
Results Page
<section>
<error v-if="error" :error="error" />
<results v-if="!error" :loading="loading" :results="results" />
<loading v-if="loading" :percentage="loadingPercentage" />
</section>
{
components: {
error: Error,
results: Results,
},
data() {
return {
loading: false,
error: null,
results: null,
}
},
created () {
this.fetchData()
},
watch: {
'$route': 'fetchData'
},
methods: {
fetchData () {
this.error = this.results = null
this.loading = true
getResults((err, results) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
this.results = results
}
})
}
}
}
Results Component
Basically the exact same results component you already have, but if loading is true, or if results is null, however you prefer, you can create a fake dataset to iterate over and create skeleton versions, if you'd like to. Otherwise, you can just keep things the way you have it.
Another option is to use the History API.
Once you are in the Results screen, you can utilize the ReplaceState to replace the URL in history of the browser.
This can be done with the beforeEach hook of the router.
What you need to do is you must save a variable globally or in localStorage in the loading component when the data is loaded (before redirecting to the results component):
export default {
name: "results",
...
importantTask() {
// do the important work...
localStorage.setItem('DATA_LOADED', JSON.stringify(true));
this.$router.push({path: "/results/xxxx"});
}
}
And then you should check for this variable in the beforeEach hook and skip to the correct component:
// router.js...
router.beforeEach((to, from, next) => {
const dataLoaded = JSON.parse(localStorage.getItem('DATA_LOADED'));
if (to.name === "loading" && dataLoaded)
{
if (from.name === "results")
{
next({ name: "main"} );
}
if (from.name === "main")
{
next({ name: "results"} );
}
}
next();
});
Also, do remember to reset the value to false in your main component when the query button is clicked (before routing to the loading component):
export default {
name: "main",
...
queryButtonClicked() {
localStorage.setItem('DATA_LOADED', JSON.stringify(false));
this.$router.push({path: "/loading"});
}
}
Your loading screen should not be controlled by vue-router at all.
The best option is to use a modal/overlay for your loading screen, controlled by javascript. There are lots of examples around for vue. If you cant find anything then vue-bootstrap has some easy examples to implement.