Nextjs getInitialProps blocked the page rendering in client side? - javascript

Since I like to add SSR to my upcoming project to improve SEO, I would like to try out next. What I want is that only use SSR for the initial page, and the rest of navigations in the site will be client side rendering. I see the getInitialProps fit the most in this case, accordingly the documentations.
As my understanding, getInitialProps is run in server for the initial page rendering, and is run in the browser when navigating using next/link. The issue I found is that the getInitialProps seems to block the page rendering. (i.e. page changed/rendered after getInitialProps is completed)
import axios from 'axios'
function Posts(props) {
return (
<div>
<div>Posts:</div>
<div>
{JSON.stringify(props)}
</div>
</div>
)
}
Posts.getInitialProps = async (context) => {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
// Wait longer to see the effect
// await (new Promise((resolve) => {
// setTimeout(resolve, 5000)
// }))
return {
props: {
posts: response.data
}
}
}
export default Posts;
How can I do it like in pure React, render the jsx first, then fill in the props? (the execution JSON.stringify(props) might be ignored at first)
Also, in next 9.3, the team introduced getServerSideProps, which is recommended over getInitialProps. How can they be comparable when they are not the same that getServerSideProps will on run in server?

Based on your comments, you want to do the fetch on the server, on the initial page load. However, if navigating between pages you don't want rendering to block while waiting for getInitialProps to return.
One solution is to check if you're on the server, and do the fetch in getInitialProps. If on the client, don't do the fetch in getInitialProps and instead fetch using useEffect in your render method.
import {useEffect} from 'react'
import axios from 'axios'
const isServer = () => typeof window === 'undefined'
const getPosts = () => {
return axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => response.data)
}
function Posts({posts}) {
const [renderPosts, setRenderPosts] = useState(posts)
useEffect(() => {
if(posts === null) {
getPosts()
.then(setRenderPosts)
}
}, [])
return (
<div>
<div>Posts:</div>
<div>
{JSON.stringify(renderPosts)}
</div>
</div>
)
}
Posts.getInitialProps = async (context) => {
if(isServer()) {
return {
posts: await getPosts(),
}
}
else {
return {
posts: null,
}
}
}
export default Posts
By the way, you may be tempted to use getServerSideProps here, since it is only called if rendering on the server. However, when a page using getServerSideProps is rendered, it will actually make a call to the server to get data from getServerSideProps, even if you're navigating using next/link. From the Next.js 9.3 blog post:
When navigating between pages using next/link instead of executing getServerSideProps in the browser Next.js will do a fetch to the server which will return the result of calling getServerSideProps.
This would still cause the blocking issue you're wanting to avoid.
One final note, this might not be an idiomatic solution. There may be a more "standard" solution. I just wasn't able to find one. You could likely also use a wrapper around your page component that could do all of this in a more consistent way. If you use this pattern a lot, I'd recommend that.

Related

SWR not serving stale data?

I was under the impression SWR should return the stale data on page load before updating the view with the new info from the API.
I have made a basic Nextjs app with simple API timeout and it's "loading" - for the 5 second timeout I have added to the API - every time. I was under the impression it should serve the previously cached version before updating once the API call returns?
Vercel URL - https://swr-test-mkhx72bz3-webknit.vercel.app/
repo - https://github.com/webknit/swr-test
index.js
import useSWR from 'swr'
const fetcher = (...args) => fetch(...args).then(res => res.json())
export default function Home() {
const { data, error } = useSWR('/api/hello', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return (
<div>{data.num}</div>
)
}
hello.js
export default function handler(req, res) {
setTimeout(() => {
res.status(200).json({ num: Math.floor(Math.random() * 5000) })
}, 5000)
}
I'm no expert but by "every time" do you mean every time you reload the webpage? SWR wont cache the value between refreshes of the webpage for you.
Cache in this context means that two components using the same swr key ('/api/hello') will result in just one call to the api. So which ever component calls the swr first will get the api response, and the second component will get the same value from the cache.
But swr can still call the api multiple times later on to "revalidate" and send the updated response to both (or all) components that use that key.

getServerSideProps proper usage?

I have recently been trying to create a web app with NextJS. I know some basics in web development but I was a little lost when using NextJS as I didn't do any React either before.
I've tried fetching data from an API and using this data in my page. I struggled a bit but in the end I got it working with the help of getServerSideProps.
My question is, how could I use getServerSideProps multiple times in my application so that I can fetch many other routes ? I've tried using getServerSideProps in a different file, using its response in a function that I then export as a component and use it so I can "get components of getServerSideProps responses" if it makes sense, but had many different errors when trying to do so.
Could someone explain how it actually works and how I could resolve my issue, and if it doesn't work that way, how could I make it work?
Here's an example using Coinbase's API :
import { useState } from 'react'
import fetch from 'isomorphic-fetch'
export const getServerSideProps = async () => {
const res = await fetch('https://api.coinbase.com/v2/prices/ETH-USD/buy')
const data = await res.json()
return {
props: {
ethprice: data
}
}
};
I then use "ethprice" in my Home function such as :
export default function Home({ ethprice }) {
return (
[page content, divs, text etc...]
{etherprice.data.amount}
Thanks!
getServerSideProps is specific to that particular file, you can't just use it in any way you want.
const Example = (props) => {
return // this is your component
}
export const getStaticProps = async () => {
// this will provide props specifically for 'Example'
}
More than that getStaticProps will only be run once on static page generation and never again, along with fetching the props for that particular component only. So you can't get live data from it, only data required to generate the page (like page title).
You can have a look at getServerSideProps if you're looking for something more dynamic that can fetch props at runtime. After that you can pass those props down to children if you need to.

Next.js: getStaticProps not updating fetch values in production

I'm basically developing a blog on Next.js. Because it is another team which is in charge of the back-end, I'm currently making fetch API calls from getStaticProps to get my articles even though it's better practice to make database queries directly:
export async function getStaticProps({ params, res }) {
try {
const result = await fetch(`${API.baseURL}/get_article/${params.id}`);
const article = await result.json();
return {
props: { article },
};
} catch (error) {
res.statusCode = 404;
return { props: {} };
}
}
While this works perfectly in development mode, getting the article, editing it, and then accessing it again does not work in production (even locally, with the built version).
I guess it has something to do with Next.js handling cache somehow... What am I doing wrong? Thank you!
First of all the argument of function getStaticProps i.e the context object doesn't have any property named res. So res.statusCode = 404; doesn't do anything here.
And getStaticProps is meant be used for static site generation, additionally for dynamic routes, you can export another function getStaticPaths which should generate and return an array of paths with the dynamic route params for which getStaticProps will be called at build time for pre-rendering the pages.
In development mode, data-fetching methods will be called per-request basis so your code works. But in production mode, it will show the pre-rendered static pages which means the page will show the content as it was rendered and if you edit and update the content it will not reflect on the pages.
If you decide to go with static-site-generation either you have to rebuild the entire site after an update to a blog or you have to have some kind of client-side data-fetching logic that will update the blog when you update its content.
For client-side data-fetching you can use something like swr or react-query
Here is some psuedo-code which might help with pre-rendering the pages,
for route /article/[articleId]
export async function getStaticPaths() {
const articles = await /* db query to get the list of articles or fetch from remote API*/
// generate a list of paths with route params
const paths = articles.map(article => ({ params: { articleId: article.id }}))
return {
paths,
fallback: false
// fallback can be true if you want to show a fallback version of page
// and serve JSON for unknown articles
}
}
export async function getStaticProps(ctx) {
try {
const result = await fetch(`${API.baseURL}/get_article/${params.id}`);
const article = await result.json();
return {
props: { article },
};
} catch (error) {
return {
props: null
}
}
}
Learn more about how fallback works in returned value of function getStaticPaths docs.
Another alternative is to use getServerSideProps as the data-fetching method which will be called on each request for the page, but TTFB(time to first byte) will be higher. So for a blog-post site, I will not suggest using getServerSideProps.
You have to add the revalidate parameter. Find out more here.
In your case
export async function getStaticProps({ params, res }) {
try {
const result = await fetch(`${API.baseURL}/get_article/${params.id}`);
const article = await result.json();
return {
props: { article },
revalidate: 10 // 10 seconds
};
} catch (error) {
res.statusCode = 404;
return { props: {} };
}
}
Please note that revalidate parameter.
There is a way to update the HTML that is generated using getStaticProps, this process is called incremental server regeneration. This will ensure that your page is updated whenever you push an update to your blog post.
NextJS has documented this
https://nextjs.org/docs/basic-features/data-fetching
You search for ISR on the above page to understand how it's done
Basically you'll have to specify a time after which NextJS will try to update the page and in case there is a new post altogether, it will be server rendered on first request and then cached, once cached it'll work almost like a static page, refer fallback: 'blocking'
Quoting from website :
Next.js allows you to create or update static pages after you’ve built your site. Incremental Static Regeneration (ISR) enables you to use static-generation on a per-page basis, without needing to rebuild the entire site. With ISR, you can retain the benefits of static while scaling to millions of pages.
Consider our previous getStaticProps example, but now with Incremental Static Regeneration enabled through the revalidate property

React Hooks: setState, when does it actually take effect?

So I'm trying to understand React Hooks and how to use them. I can make a fetch inside a component as follows:
var [pages,setPages] = useState();
var [page,setPage] = useState();
async function fetchData() {
await fetch(`https://myBackend/pages`)
.then(response => response.json())
.then(response => {
setPages(response);
console.log(pages[0].title);
})
.then(()=>{
// AT THIS STAGE, pages IS UNDEFINED
setPage(pages.filter(singlepage=> {
return singlepage.url == props.match.params.url;
}))
}
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
console.log(pages[0].title);
return () => {
console.log('unmounting...');
}
},[]);
I call to my backend to get all the pages, which works fine, as if I harcode pages[0].title it will render. But when I'm trying to access specific values in pages within the useEffect hook, for me to assign page, it gives me the error of pages being undefined. My console logging makes this apparent.
I want page as well as pages to be defined on 'component mounting', prior to the page loading. So my question is when does setPages actually set the page? and is it possible for me to assign both within useEffect, and based off each other?
Asynchronous actions take time. An indeterminate amount of time could theoretically pass. So, there's no way to guarantee that your data fetches before mounting your component.
Instead, you need to act as if the data could be undefined and have a state of the application that handles that.
As for getting access to the pages variable immediately after calling setPages, that will also fail because React actually runs the re-render asynchronously.
Even if it ran at the same time, there's a thing called closures in which javascript pulls in all variables around a function when it is created, so that the function always has access to those. React Hooks work by utilizing these closures to only have access to the variables as they were when the component was rendered/re-rendered. The reason this is the case, is because every re-render, all of the functions in your component are re-created which creates a new closure.
So, this means that you'll need to keep these concepts in mind as you work with React.
As for your code, the results solution that Dlucidione set up is one of your best bets, aside from setting up a separate useEffect to update page when pages changes.
const history = useHistory(); //from react-router-dom
useEffect(() => {
async function fetchData() {
return await fetch(`https://myBackend/pages`)
.then((response) => response.json())
.then((response) => {
setPages(response);
})
.catch((err) => setErrors(err));
}
fetchData();
return () => {
console.log('unmounting...');
};
}, []);
useEffect(() => {
const url = props.match.params.url;
if (!pages || !url) return;
// Using Array#find here because I assume based on the name, you want one page rather than an array
const page = pages.find((singlepage) => {
return singlepage.url === url;
});
if (!page) {
// This will change the route if a page isn't found.
history.push('/404');
}
setPage(page);
}, [pages, props.match.params.url, history]);
Redirect and history.push are different in how they work. Redirect is the declarative navigation method, while history.push is the programmatic.
Example usage of history.push:
if (!page) {
// This will change the route if a page isn't found.
history.push('/404');
}
Important note, this can be used anywhere in the code as long as you can pass the history object to there. I've used it within redux thunks before as well.
Example usages of Redirect without it redirecting on mount:
if(!pages && !page){
return <Redirect to="/404"/>
}
Or within some JSX:
<div>
{!pages && !page && <Redirect to="/404"/>}
</div>
Redirect has to be rendered which means its only usable within the return statement of a component.
The way I'm understanding it: the redirect is based on if the
individual page is found in the mounting process, therefore the
redirecting process has to also be in the mounting process to check
before rendering anything to redirect or not.
This is correct. When the Redirect itself mounts or updates, it will redirect if the conditions are correct. If there's a path or from prop on the Redirect (they are aliases of each other), then that limits the Redirect to only work when that path matches. If the path doesn't match, the redirect will do nothing until the path is changed to match (or it unmounts, of course).
Under the hood, Redirect just calls history.push or history.replace in componentDidMount and componentDidUpdate (via the Lifecycle component), so take that as you will.
useEffect(() => {
const fetchData = async () => {
try {
// Make a first request
const result = await axios.get(`firstUrl`);
setPages(result);
// here you can use result or pages to do other operation
setPage(result.filter(singlepage=> {
return singlepage.url == props.match.params.url;
or
setPage(pages.filter(singlepage=> {
return singlepage.url == props.match.params.url;
}))
} catch (e) {
// Handle error here
}
};
fetchData();
}, []);

How can I use getInitialProps only during the NextJS site build?

When using NextJS to build a static site, I would like the getInitialProps method to fire only during the build step and not on the client.
In the build step, NextJS runs the getInitialProps method before each component's rendered HTML is used to generate the page's static HTML. On the client, NextJS also runs this method before the page component is rendered in order to return the necessary props for the component. Thus, large requests can delay the client's first paint as this is a blocking request.
// example usage of API call in getInitialProps
import fetch from 'isomorphic-unfetch'
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async ({ req }) => {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
export default Page
I'm unwilling to move my slow API request to componentDidMount in order to avoid the blocking request because I want to use the data returned during the build step to populate the static HTML, and this particular request doesn't need to be dynamic or updated after the build.
Is there a way I can make getInitialProps run only when next export builds and not as the client loads the page?
Is this good practice?
I found the workaround with NextJs 9.0.3 (other versions may also work, I didn't test that)
// XXXPage is your page
XXXPage.getInitialProps = async (req) => {
if (process.browser) {
return __NEXT_DATA__.props.pageProps;
}
// original logic
}
For version 9.3 or newer, it's recommended that you use getStaticProps for providing static build props.
export async function getStaticProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
Old answer
There are two ways is one way that I've found to prevent code in getInitialProps from running on a page component load.
1. Use a regular anchor tag without next/link to that page.
getInitialProps only runs when the page is linked from a next/link component. If a regular JSX anchor click me is used instead, the component's getInitialProps will not be invoked. Direct page loads to NextJS static site pages will not invoke getInitialProps.
Note that using a standard anchor instead of the next/link component will cause a full page refresh.
Because this is a poor solution, I've submitted a feature request.
2. Use req in the context argument to conditionally make the API call in getInitialProps.
I believe what #evgenifotia wanted to convey is that req is undefined in a site that's been exported.
// example usage of API call in getInitialProps
import fetch from 'isomorphic-unfetch'
function Page({ stars }) {
return <div>Next stars: {stars}</div>
}
Page.getInitialProps = async (ctx) => {
const { req } = ctx // context object: { req, res, pathname, query, asPath }
if (req) { // will only run during the build (next export)
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const json = await res.json()
return { stars: json.stargazers_count }
}
return {}
}
export default Page
For more information about getInitialProps, see the documentation. One example there confirms that req is expected to only be defined on the server (or during the exporting build):
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent`
This second option may work for some scenarios, but not mine where returning an empty result from getInitialProps will affect the component's this.props.
Note:
Shallow routing is not the answer. According to the documentation (see under "Notes" section):
Shallow routing works only for same page URL changes.
A more detailed and updated version as of the accepted answer:
const isInBroswer = typeof window !== 'undefined';
if (isInBroswer) {
const appCustomPropsString =
document.getElementById('__NEXT_DATA__')?.innerHTML;
if (!appCustomPropsString) {
throw new Error(`__NEXT_DATA__ script was not found`);
}
const appCustomProps = JSON.parse(appCustomPropsString).props;
return appCustomProps;
}

Categories

Resources