Fetching Data from API using NextJS and Material UI React - reactjs

I am trying to create dynamic pages that shows individual book details (i.e. title/author etc) on a separate page based on a query string of the "id" of each book. However, I am having difficulty in understanding how to make a request to a API endpoint using NextJS that will get the book details based on its "id". I would like to use Material UI as a UI Framework.
ISSUE: When I run npm run dev the book page loads but the book's "props" are not being passed along to the BookAttributes component. The console.log(book) I added in the book page is undefined and the console.log(title) in BookAttributes is undefined as well.
I've tested the API endpoint in POSTMAN and it appears to work.
When I refactor the same code using Semantic UI-React instead of Material UI, the book pages load correctly.
I am using the NextJS Material UI starter template from the Material UI website as a baseline.
I am fairly new to NextJS and Material UI so your assistance and guidance would be greatly appreciated. Thank you for your help on this!
Here is the code I have so. I have tried to keep in clean and simple.
BOOK PAGE (within 'pages' directory)
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
function Book({ book }) {
console.log(book)
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
Book.getInitalProps = async ({ query: { _id } }) => {
const url = 'http://localhost:3000/api/book';
const payload = { params: { _id }}
const response = await axios.get(url, payload)
return { book: response.data }
}
export default Book;
BOOK API ENDPOINT (within 'pages/api' directory)
import Book from '../../models/Book';
import connectDb from '../../utils/connectDb';
connectDb()
export default async (req, res) => {
const { _id } = req.query
const book = await Book.findOne({ _id })
res.status(200).json(book);
}
BOOK ATTRIBUTE COMPONENT (within 'components' directory)
import React from 'react';
function BookAttributes({ title }) {
console.log(title)
return (
<>
<h1>{title}</h1>
</>
)
}
export default BookAttributes;

You should be using dynamic routes here if you want to work with data-fetching methods like getStaticProps or getServerSideProps.
You can create a page like pages/book/[id].js. But to generate the page you have to decide what data-fetching method you want to run. If the data for the page doesn't change very often you can choose to use static-site-generation using getStaticProps which will generate the pages at build time. If the data will be changing a lot you can either do server-side-rendering using getServerSideProps or fetch the data client-side.
Here is an example for your use-case that you can use for server-side-rendering using getServerSideProps, keep in mind the API call inside getServerSideProps might fail so you should have appropriate error handling.
In pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getServerSideProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
Using static-site-generation
Because the page is dynamic you have to provide a list of paths for which nextjs will generate the pages. You can do that by exporting an async function called getStaticPaths.
in pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getStaticPaths = async () => {
// here you have two options if you know all the ids of the books
// you can fetch that data from the api and use all the ids to generate
// a list of paths or show a fallback version of page if you don't know all
// ids and still want the page to be static
// Pseudo code might look like this
const res = await axios.get('api-endpoint-to-fetch-all-the-books')
const paths = res.data.map(book => ({ params: { id: book.id }}))
return {
paths,
fallback: false
}
}
export const getStaticProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
The fallback property in the returned value of getStaticPaths is somewhat important to understand. If you know all the necessary id for the pages you can set the fallback to false. In this case nextjs will simply show a 404 error page for all the paths that were not returned from the function getStaticPaths.
If fallback is set to true nextjs will show a fallback version of page instead of a 404 page for the paths that were not returned from the getStaticPaths function. Now where should you set fallback to true? Let's suppose in your case new books are added to the database frequently, but the data for the books doesn't change very often so you want the pages to be static. In this case, you can set fallback to true and generate a list of paths based on avaliable book ids. For the new books nextjs will first show the fallback version of the page than fetch the data based on the id provided in the request and will send the data as JSON which will be used to render the page in the client.

Related

How I can get localstorage data inside getServerSideProps

I am using NextJS 12. I am trying to get local storage object. When I use localstorage inside getServerSideProps I get an error like this ReferenceError: localStorage is not defined. I tried to use it outside the function as well but I still get this error. Is there any way to use it inside getServerSideProps.
export async function getServerSideProps({ query }) {
const id = query.id;
const getData = JSON.parse(localStorage.getItem("form"));
console.log(getData)
return {
props: {},
}
Welcome to StackOverflow, as it refers in the documentation
If you export a function called getServerSideProps (Server-Side Rendering) from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps.
Localstorage is only available on the client side and you are trying to access it in a server side only function , you can use something like
if (typeof window !== 'undefined') {
// your code
const id = query.id;
const getData = JSON.parse(localStorage.getItem("form"));
console.log(getData)
}
Please review this article to get more information on running client side only code.
Another approach would be to use a dynamic import where the hello3 component would contain the code accessing local storage.
import dynamic from 'next/dynamic'
const DynamicComponentWithNoSSR = dynamic(
() => import('../components/hello3'),
{ ssr: false }
)
function Home() {
return (
<div>
<Header />
<DynamicComponentWithNoSSR />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
Another way would be using cookies instead of using localstorage, I had the same problem when I developed my last application and I solved it using the nookies package
Nookies: A collection of cookie helpers for Next.js

Error: getStaticPaths is required for dynamic SSG pages and is missing for "xxx". NextJS

I am getting this error "Error: getStaticPaths is required for dynamic SSG pages and is missing for 'xxx'" when I try to create my page in NextJS.
I don't want to generate any static page on build time. So why do I need to create a 'getStaticPaths' function?
If you are creating a dynamic page eg: product/[slug].tsx then even if you don't want to create any page on build time you need to create a getStaticPaths method to set the fallback property and let NextJS know what to do when the page you are trying to get doesn't exist.
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
return {
paths: [], //indicates that no page needs be created at build time
fallback: 'blocking' //indicates the type of fallback
}
}
getStaticPaths does mainly two things:
Indicate which paths should be created on build time (returning a paths array)
Indicate what to do when a certain page eg: "product/myProduct123" doesn't exist in the NextJS Cache (returning a fallback type)
Dynamic Routing Next Js
pages/users/[id].js
import React from 'react'
const User = ({ user }) => {
return (
<div className="row">
<div className="col-md-6 offset-md-3">
<div className="card">
<div className="card-body text-center">
<h3>{user.name}</h3>
<p>Email: {user.email} </p>
</div>
</div>
</div>
</div>
)
}
export async function getStaticPaths() {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const users = await res.json()
const paths = users.map((user) => ({
params: { id: user.id.toString() },
}))
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`)
const user = await res.json()
return { props: { user } }
}
export default User
For rendering dynamic route use getServerSideProps() instead of getStaticProps()
For Example:
export async function getServerSideProps({
locale,
}: GetServerSidePropsContext): Promise<GetServerSidePropsResult<Record<string, unknown>>> {
return {
props: {
...(await serverSideTranslations(locale || 'de', ['common', 'employees'], nextI18nextConfig)),
},
}
}
You can check here as well
if you are using getStaticPaths, you are telling next.js that you want to pregenerate that page. However since you used it inside a dynamic page, next.js does not know in advance how many pages it has to create.
with getStaticPaths, we fetch the database. If we are rendering blogs, we fetch the database to decide how many blogs we have, what would be idOfBlogPost and then based on this information, getStaticPath will pre-generate pages.
also, getStaticProps does not run only during the build time. If you add revalidate:numberOfSeconds, next.js will recreate new page with fresh data after "numberOfSeconds" time.
You're rendering a dynamic route so use getServerSideProps() instead of getStaticProps()

Creating Dynamic Routes using 'getStaticPaths' and 'getStaticProps' in NextJS

I am trying to create dynamic pages that show individual book details (.i.e. title/author) on a separate page based on a query string of the "id" for each book. In a previous question I asked, answers from users were very helpful and I have a much better understanding of how to use getStaticPaths and getStaticProps correctly. However, I am not quite there in my code for how to do this.
Here is the basic setup and context.
I am running NextJS 9.4 and would like to use a API endpoint instead of querying the database directly.
The book data is being pulled from a MongoDB Atlas Database and uses Mongoose
Documents in the MongoDB have a "_id" as a unique ID.
I have tried to incorporate and learn from existing Github examples and NextJS documentation but I still get the following error.
Error: A required parameter (id) was not provided as a string in getStaticPaths for /book/[id]
Here is the code I have so far. I have tried to keep the code as clean as possible for now.
export default function Book({ book }) {
return (
<article>
<h1>Book Details Page</h1>
<p>{book.title}</p>
<p>{book.author}</p>
</article>
)
}
export async function getStaticPaths() {
const url = `${baseUrl}/api/books/books`
const response = await axios.get(url);
const books = response.data
const paths = books.map((book) => ({
params: { id: book.id },
}))
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
const url = `${baseUrl}/api/books/books/${params.id}`
const res = await axios.get(url)
const book = await res.json()
return { props: { book }}
}
The API endpoint looks like this:
import Book from '../../../models/Book';
import dbConnect from '../../../utils/dbConnect';
// conects to the database
dbConnect();
// This gets all the book from the database
export default async (req, res) => {
const books = await Book.find()
res.status(200).json(books)
}
Any support or feedback would be greatly appreciated. Once I get this working, I can hopefully be able to understand and help assist others in creating dynamic routes with NextJs. Thank you.
You can't make calls to Next.js API routes inside getStaticProps or getStaticPaths. These functions are executed at build time, so there is no server is running to handle requests. You need to make request to DB directly.
If you want to keep it clean you could create a helper module like allBooksIds() and keep DB query in a separate file.
See the same issue - API call in NextJS getStaticProps
Simply add toString() method in getStaticPaths because the book id is of type ObjectID("ID") if you do params: { id: book._id.toString() } it will convert ObjectID("ID") to type string which is accepted by getStaticPaths().The complete code for the nextjs part is below also update your API route as follows :-
The upper one is the API route the bellow one is Nextjs Page
import Book from '../../../models/Book';
import dbConnect from '../../../utils/dbConnect';
// conects to the database
dbConnect();
// This gets all the book from the database
export default async (req, res) => {
const books = await Book.find({})
res.status(200).json(books)
}
export default function Book({ book }) {
return (
<article>
<h1>Book Details Page</h1>
<p>{book.title}</p>
<p>{book.author}</p>
</article>
)
}
export async function getStaticPaths() {
const url = `${baseUrl}/api/books/books`
const response = await axios.get(url);
const books = response.data
const paths = books.map((book) => ({
params: { id: book._id.toString() },
}))
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
const url = `${baseUrl}/api/books/books/${params.id}`
const res = await axios.get(url)
const book = await res.json()
return { props: { book }}
}
Hope this is helpful

Fetch data using an id from an api but display an alternative text to address bar e.g. content_title in Next.js

I'm using an api that only allows fetching data using an id BUT NOT title as follows http://0.0.0.0:5000/blog/13b03a39-bc04-4604-baf6-059658f9f5e8 . The endpoint returns a JSON object which I want to render to browser using Next Js. However I want to have a clean url architecture that contains title as follows:
instead of a url containing the id as follows:
Here's how I'm passing props to Link :
<Link
href={`/post/${post.blog_title}`}
as={`/post/${post.blog_title}`}>
<a className='blog__card--link'>
<h4 className='blog__card--title'>{post.blog_title}</h4>
</a>
</Link>
and here's is my [id].js file:
import { getBlog } from '../../api/index';
const Article = ({ blog }) => {
const router = useRouter();
return (
<div class='article'>
<div class='article__main'>
{/* {console.log(router)} */}
<ArticleView />
</div>
</div>
);
};
Article.getInitialProps = async (router) => {
const res = await getBlog(`${router.query.id}`);
const json = await res.json();
console.log(json);
return { blog: json };
};
export default Article;
Trying to access ${router.query.id} in getInitialProps it returns the title which I understand is what I'm passing through as prop in Link.
Is it possible to achieve a clean url structure but also use id in getInitialProps? and how can I achieve it using dynamic links in Next.js ?
IMPORTANT UPDATE
-> The only good alternative is to use slugs in the API because imagine someone follows a link to your post from another site or perhaps enter the URL in the browser address bar? How would you know what to query for from the backend? Therefore this question comes from a place of impracticality , choosing to retain it on the site for the sake of others in the future.
you need to import Next/Router
import { useRouter } from 'next/router'
change your getInitialProps function to
Article.getInitialProps = async (router) => {
//get id query parameter
const res = await getBlog(`${router.query.id}`);
const json = await res.json();
console.log(json);
return { blog: json };
};
this should work for you
https://nextjs.org/docs/api-reference/data-fetching/getInitialProps#context-object
Article.getInitialProps = async function(context) => {
const res = await getBlog(`${context.query.id}`);
const json = await res.json();
console.log(json);
return { blog: json };
};
solution 1
however this base on your endpoint my suggest is change endpoint to receive slug so you can manage your slug
solution 2
not best practice but you can get clean url and get data by id Using Redux
solution 3
using next-routes using next-routes and pass id as params
<Link route='blog' params={{id: data.id}}>
<a>Hello world</a>
</Link>
What I was trying to achieve in this question is not practical therefore I settled to having unique slugs in the back-end that can be used to fetch the specific data.
Take a scenario where a link to your article/post is shared somewhere else, then the only way to get data to that post/article is using a slug.

Getting 404 when first loading dynamic routes on nextjs

I'm trying to create a blog page to test nextjs and created a dynamic route for the posts, which will be retrieved from Contentful. When navigating from Home page and clicking into a next/router <Link /> component, the blog post loads correctly, but if I get the URL and try loading the page directly from browser address bar, I'll get 404.
Steps to reproduce:
1. git clone https://github.com/zeit/next-learn-demo.git
2. cd next-learn-demo/8-deploying
3. yarn
4. next build && next export
5. cd out
6. serve
7. Navigate to http://localhost:5000/p/learn-nextjs
8. See 404
Is this a limitation of NextJS (didn't find anything related to it on documentation) or do we need to configure anything else?
The real issue is that exporting a next app will make it generate static HTML files. Even though it will still be able to request data before rendering the page, the set of available paths are not dynamic (they are generated during the next export command). See this docs and this example.
Based on this, I have 2 possible solutions:
generate a webhook to trigger a next build && next export command every time a new blog post is published in Contentful;
avoid exporting my next app and host a Node server that will handle the dynamic routes.
That's because when you directly access the link or refresh the page then it add's a slash at the end of route. An next.js doesn't recognize any route like that. To fix this, I hope there should be an easiest way to do that. However you can do this using custom server. Here is an example:
server.get("/about/", (req, res) => {
return app.render(req, res, "/about")
});
Hope this will help you.
To extend the answer provided by #Minoru, the official Next documentation covers this case in this example.
Using getStaticPaths and getStaticProps allows you to create dynamic pages at build time, avoiding the 404.
Example code for posts dynamic page:
import { GetStaticPaths, GetStaticProps } from 'next';
const Post = ({ post }) => {
const { id, content } = post;
return (
<div>
<h1>Post {id}</h1>
<p>{content}</p>
</div>
);
};
export const getStaticPaths: GetStaticPaths = async () => {
// Get all posts via API, file, etc.
const posts = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }]; // Example
const paths = posts.map(post => ({
params: { id: post.id },
}));
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps = async context => {
const postId = context.params?.id || '';
// Get post detail via API, file, etc.
const post = { id: postId, content: `I'm the post with id ${postId}!` }; // Example
return { props: { post } };
};
export default Post;
When building the site with next build && next export we will see in the out folder that Next has created each post page
Then, when you navigate to /posts/3/ you will see the post with id 3
For reference, this docs page contains this case and many other use cases.
Don't want to infringe any old posts rules, but in case anyone else in my context I use vercel's feature webhook to new deploys and as I was using firebase I've created a simple firebase function whith is hooked to a new event creation of a page triggers the webhook. I've used fetch because we can make a GET request according to the docs
exports.newEventAdded = functions.region('us-central1').firestore.document('local_events/{localeventId}')
.onCreate((snap, context) => {
fetch('https://api.vercel.com/v1/integrations/deploy/process.env.NEXT_PUBLIC_VERCEL_WEBHOOK_ID')
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(JSON.stringify(myJson));
});
})

Resources