Dynamic routing with getServerSideProps in Nextjs - javascript

I'm trying to learn nextjs. Struggling to work out routing with getServerSideProps.
Using a free API I have a list of countries displayed on the DOM. I want to dynamically link to a country and data be fetched and displayed for that specific country.
Heres my code so far
const Country = props => (
<Layout>
<h1>{props.country.name}</h1>
<span>{props.country.capital}</span>
</Layout>
);
export async function getServerSideProps(context) {
const { id } = context.query;
const res = await fetch(`https://restcountries.eu/rest/v2/name/${id}`);
const country = await res.json();
console.log(`Fetched place: ${country.name}`);
return { props: { country } };
}
export default Country;
<div className='container'>
<Head>
<title>Countries List</title>
<link rel='icon' href='/favicon.ico' />
</Head>
<Layout>
<main>
<h1>
Countries{' '}
<span role='img' aria-label='world emoji'>
🌎
</span>
</h1>
<ul>
{countries.map(country => (
<li key={country.name}>
<Link href='/p/[id]' as={`/p/${country.name}`}>
<a>{country.name}</a>
</Link>
</li>
))}
</ul>
</main>
</Layout>
</div>
);
export async function getServerSideProps() {
// Call an external API endpoint to get posts.
const res = await fetch('https://restcountries.eu/rest/v2/all');
const countries = await res.json();
// By returning { props: posts }, the Blog component
// will receive `posts` as a prop at build time
return {
props: {
countries,
},
};
}
export default Home;
The URL dynamically routes ok. For example, when you click on Afghanistan the URL shows http://localhost:3000/p/Afghanistan.
My country component however doesn't display anything and undefined is printed to the terminal.
Example of URL and response from URL: https://restcountries.eu/rest/v2/name/Afghanistan
{
name: "Afghanistan"
}
Apologies if a noob question. Trying to learn nextjs

export async function getServerSideProps(context) {
const { id } = context.query;
const res = await fetch(`https://restcountries.eu/rest/v2/name/${id}`);
const country = await res.json();
console.log(`Fetched place: ${country.name}`);
return { props: { country } };
}
you are returning a nested object from above function
{ props: { country:country } }
so this prop will be attached to props as like this:
`props.props`
this is how you should implement
const Country = props => (
<Layout>
<h1>{props.props.country.name}</h1>
<span>{props.props.country.capital}</span>
</Layout>
);
UPDATE
In early version of next.js I think updated after version 9, we were not returning from serverside function by using props. As of now correct way of implementation is
return {
props: {
countries,
},
};
Next.js 13 Update
In next.js 13, if you set app directory, components in this directory will be server-rendered components by default. That means everything will be run on the server and we do not need to write specifiacallygetServerSideProps. in "app" directory, if your file name is surrounded by [..id], it means it is a dynamic route. In page.jsx, you can access id like this
export default function ProductPage({ params }) {
return (
<div>
<h1>Product ID: {params.id}</h1>
</div>
);
}

There's nothing wrong in how you're handling the dynamic routing of the page. The issue is that the data returned by the API is an array but your code expects it to be an object. You can retrieve the first item from the array and pass that to the component from getServerSideProps.
export async function getServerSideProps(context) {
const { id } = context.params; // Use `context.params` to get dynamic params
const res = await fetch(`https://restcountries.com/v2/name/${id}`); // Using `restcountries.com` as `restcountries.eu` is no longer accessible
const countryList = await res.json();
const [country] = countryList; // Get first item in array returned from API
return { props: { country } };
}
const Country = ({ country }) => {
console.log(country);
return (
<>
<h1>{country.name}</h1>
<span>{country.capital}</span>
</>
);
};
export default Country;

Just to add to the accepted answer, you could also destructure to make it (imho) more readable. This is entirely optional though
const Country = ({ country }) => (
<Layout>
<h1>{country.name}</h1>
<span>{country.capital}</span>
</Layout>
);

Related

Next.js, getStaticProps not working with component but does with page

If I visit this code on local host, it is able to pull data from the API and then display it on a card.
import { formatNumber, parseTimestampJM } from '../../utils';
import { Card } from './UserTransactions.styled';
// STEP 1 : fetch data from api
export async function getStaticProps() {
const res = await fetch(
'https://proton.api.atomicassets.io/atomicmarket/v1/sales'
);
const data = await res.json();
return {
props: {
data,
},
};
}
function UserTransactionsComponent({ data }) {
const results = data;
console.log(results);
return (
<PageLayout>
<div>
<h1>This is a list of User Transactions!</h1>
</div>
<ul>
{results.data.map((result) => {
const {
sale_id,
buyer,
seller,
listing_price,
listing_symbol,
created_at_time,
} = result;
if (buyer !== null) {
return (
<Card>
<li key={sale_id}>
<h3>
{seller} just sold item number {sale_id} to {buyer} for{' '}
{formatNumber(listing_price)} {listing_symbol} at{' '}
{parseTimestampJM(created_at_time)}
</h3>
</li>
</Card>
);
}
})}
</ul>
</PageLayout>
);
}
export default UserTransactionsComponent;
When I create a component and then call it in to my index page like so:
<PageLayout>
<Banner modalType={MODAL_TYPES.CLAIM} />
<ExploreCard />
<HomepageStatistics />
<Title>New & Noteworthy</Title>
<UserTransactionsComponent />
<Grid items={featuredTemplates} />
</PageLayout>
);
};
export default MarketPlace;
it gives me the following error
TypeError: Cannot read properties of undefined (reading 'data')
27 | <ul>
> 28 | {results.data.map((result) => {
| ^
29 | const {
30 | sale_id,
31 | buyer,
I think that the reason I'm getting this error is because of the way the data is being fetched. Perhaps it's not being included in the component.
getStaticProps works only for page components inside pages folder. And the data is fetched at build time. If you wanna use UserTransactionsComponent as a normal component, you should use useEffect and make the API call on mount.
Here is what Next.js's documentation says about getStaticProps:
If you export a function called getStaticProps (Static Site Generation) from a page, Next.js will pre-render this page at build time using the props returned by getStaticProps.
Here is UserTransactionsComponent as a normal component:
import {useState, useEffect} from "react"
function UserTransactionsComponent() {
const [data, setData]=useState();
useEffect(()=>{
async function fetchData() {
const res = await fetch(
'https://proton.api.atomicassets.io/atomicmarket/v1/sales'
);
const {data} = await res.json();
setData(data)
}
fetchData()
},[]);
if(!data){
return (<div>Loading...</div>)
}
return (
<PageLayout>
<div>
<h1>This is a list of User Transactions!</h1>
</div>
<ul>
{data.map((result) => {
const {
sale_id,
buyer,
seller,
listing_price,
listing_symbol,
created_at_time,
} = result;
if (buyer !== null) {
return (
<Card>
<li key={sale_id}>
<h3>
{seller} just sold item number {sale_id} to {buyer} for{' '}
{formatNumber(listing_price)} {listing_symbol} at{' '}
{parseTimestampJM(created_at_time)}
</h3>
</li>
</Card>
);
}
})}
</ul>
</PageLayout>
);
}
export default UserTransactionsComponent;

Next js pass JSON object from one page to another page props when navigating programmatically

I have two pages in a Next.js project, in the first one the user fills out a form to create a post, that info gets stored in a JSON object that has to be passed to the second page so the user can see a preview of the post and if he/she likes it, the post gets created.
The object is a little to big to be passed as query parameters (it has around 25 elements in it) but I can't find how to pass it to the second page props.
The function that gives the object it's values looks like this
async function submitHandler(event) {
event.preventDefault();
validateAndSetEmail();
validateAndSetTitle();
validateAndSetSalary();
if (isFormValidated === true) {
// this checks if the user has created another post
const finalPostData = await checkExistence(Email);
if (finalPostData["user"] === "usernot found") {
setPostData({
title: Title,
name: Name,
city: City,
email: Email,
type_index: TypeNumber,
type: Type,
region_index: regionNumber,
region: region,
category_index: categoryNumber,
category: category,
range: Range,
highlighted: highlighted,
featured: featured,
show_logo: showLogo,
show_first: showFirst,
description: Description,
price: basePrice + cardPrice,
website: Website,
logo: Logo,
application_link: applicationLink,
time_to_pin: weeksToPin,
pin: pin,
post_to_google: shareOnGoogle,
});
} else {
setPostData({
// set other values to the object
});
}
// Here I want to do something like
router.push({
pathname: "/preview/",
post: PostData,
});
}
}
The second page looks something like this:
export default function PreviewPage(props) {
const item = props.postInfo; // <= I want item to recieve the JSON object from the other page
return (
<Fragment>
<Navbar />
<div className="flex inline mt-12 ml-24">
<Card
category={item.category}
Name={item.company}
logo={item.logo}
City={item.company_city}
Website={item.company_website}
/>
<Description
title={item.job_title}
description={item.description}
category={item.category}
region={item.region}
Type={item.type}
Link={item.link}
/>
</div>
<Footer />
</Fragment>
);
}
Try
router.push({
pathname: "/preview/",
query: PostData,
});
export default function PreviewPage() {
const router = useRouter()
const item = router.query
After looking into React Context and Redux thanks to knicholas's comment, I managed to find a solution.
Created a context provider file
I create the Context with 2 things inside:
An empty object representing the previewData that will be filled later
A placeholder function that will become the setter for the value
import React from "react";
const PreviewContext = React.createContext({
Previewdata: {},
setPreviewData: (data) => {},
});
export default PreviewContext;
Then in _app.js
Create a useState instance to store the previewData and have a funtion to pass to the other pages in order to set the value when the form is filled, then, wrap the component with the context provider
function MyApp({ Component, pageProps }) {
const [previewData, setPreviewData] = useState({});
return (
<div className="">
<PreviewContext.Provider value={{ previewData, setPreviewData }}>
<Component {...pageProps} />
</PreviewContext.Provider>
</div>
);
}
export default MyApp;
In the form page
I get the function to set the value of previewData by using of useContext();
const { setJobPreviewData } = useContext(jobPreviewContext);
With that the value for previewData is available everywhere in the app

get array from single firestore document with ReactJs

I'm learning to use firebase and react. I have shared my firestore collection image. and my code for fetching the array from my document is given below.
This code is fetching the data from my firestore database and then storing the result in my watchlistMovies react state. when i try to log the react state or even data.data() it gives the desired result but when i try to map over the array or do something similar like logging watchlistMovies.myList[0].media_type it hits me with an error. i tried my best trying different things making it work but it breaks a thing or two in process.
I hope someone here will help me. Thank you in advance! : )
updated the code
const Watchlist = () => {
const [watchlistMovies, setwatchlistMovies] = useState([]);
const {currentUser} = useAuth()
const usersCollectionRef = collection(db,"users")
const docRef = doc(db,"users",currentUser.uid)
useEffect(() => {
const getWatchListMovies = async () => {
const data = await getDoc(docRef)
if (data.exists()) {
console.log(data.data());
setwatchlistMovies([...watchlistMovies ,data.data().myList])
} else {
console.log("empty");
}
}
getWatchListMovies();
}, [])
console.log(watchlistMovies);
// console.log(watchlistMovies.myList[0]);
return (
<div className="content-page-area">
<h1 className="trending-text"> My Watchlist </h1>
<Container className="watchlist-container">
<hr/>
{watchlistMovies.map(
(item) => (
<ListContent
item_poster={item.poster_url}
item_title={item.media_title}
item_year={item.release_year}
item_rating={item.media_rating}
item_type={item.media_type}
item_id={item.media_id}
/>
)
)}
</Container>
<br/>
<br/>
<br/>
</div>
)
}
export default Watchlist

NextJS - getStaticPaths - Paths seems not bound to one of the params

My Next.js app's pages are provided by an API, each with a uri_prefix and a uri.
The uri_prefix is mandatory, and can be c or s for now. But any letter could be chosen in the end.
It's a pretty simple structure :
pages
|-[prefix]
| |
| |-[slug]
The [slug].jsx page that handles it uses ISG.
So far, the getStaticPath fallback was set to true, so that if an admin creates new pages (eg. {uri_prefix : c, uri : newpage1} & {uri_prefix : s, uri : newpage2}) in the backoffice, ISG generates the static /c/newpage1 and /s/newpage2 file when they are first triggered.
But once generated, trying alt url such as /x/newpage or /whatever/newpage also fires up the page, which is somewhat unexpected (and unwanted). Reading the doc, I thought it would allow only existing prefixes.
Setting fallback to false forbids unwanted urls but also requires a new build of the app, which is not convenient.
I'd like to have /c/newpage1 or /s/newpage2 rendered, but not /c/newpage2 nor /s/newpage1 (nor /somethingelse/newpage* of course). Each page associated with it's own prefix only.
Did I misunderstood how the fallback works ?
Is ther an obvious mistake in the code or is there another way to achieve ISG for new pages without whole build while forbidding unwanted urls ?
Here is the [slug].jsx page :
import Layout from '#/components/Layout';
import SmallCard from '#/components/SmallCard';
import {useRouter} from 'next/router';
export default function src({products, seo}) {
const router = useRouter();
if(router.isFallback) {
return (
<h1>Loading new page...</h1>
)
}
return (
products ? (
<Layout title={seo[0].title} description={seo[0].description}>
<div>
<div className='container-fluid my-5'>
<div className="row justify-content-center">
<h1>{seo[0].h1}</h1>
</div>
<div className="row justify-content-center">
{products.map(
(product)=>(
<SmallCard product = {product}/>
)
)}
</div>
</div>
</div>
</Layout>
) : (
<Layout title={seo[0].title} description={seo[0].description}>
<h1>{seo[0].h1}</h1>
<div dangerouslySetInnerHTML={{__html: seo[0].content_front}}/>
</Layout>
)
)
}
export async function getStaticPaths() {
const resPages = await fetch(`${process.env.API_BASE_URL}/path/to/pagesapi`);
const pages = await resPages.json();
const paths = pages.map((page) => ({
params: {
prefix: page.uri_prefix,
slug: page.uri
},
}))
return {
paths,
fallback: true
};
}
export async function getStaticProps({ params: { slug } }) {
const resPages = await fetch(`${process.env.API_BASE_URL}/path/to/pagesapi`);
const pages = await resPages.json();
const seo=pages.filter(page=> page.uri == slug);
if(seo[0].src) {
const src=seo[0].src;
// get products
const resProducts = await fetch(`${process.env.API_BASE_URL_LEGACY}${src}`);
var products = await resProducts.json();
} else {
var products = null
}
return {
props: {
products,
seo
},
revalidate:60,
};
}
Thanks in advance.

why can i not update my state with my api response? data is an array of objects

I have two api requests that return JSON objects. They return an array of objects.
One API request that I make is fine and allows me to update the state with the response, but the other one (below) doesn't and I don't understand why.
API request to fetch genres list:
async getGenreList() {
const genresResults = await getGenres();
return genresResults;
}
The request:
export const getGenres = async () => {
try {
const response = await axios.get(
"https://api.themoviedb.org/3/genre/movie/list?api_key=<APIKEY>&language=en-US"
);
const { genres } = response.data;
return genres;
} catch (error) {
console.error(error);
}
};
The response is an array of 19 genre objects but this is just an example:
[
{id: 28, name: "Action"},
{id: 12, name: "Adventure"}
]
I then want to update the state like this and pass the response to genreOptions. But it tells me Error: Objects are not valid as a React child (found: object with keys {id, name}). If you meant to render a collection of children, use an array instead.
componentDidMount() {
this.getGenreList().then((response) => {
console.log(response)
this.setState({ genreOptions: response});
});
}
The below works when i update the state and map over it but I don't want to do that, i want to pass the whole response down so i can map over the data in my component as I need it there to do some data matching.
this.setState({ genreOptions: response.map((genreOption) => {
return genreOption.name
})});
This is the state:
this.state = {
results: [],
movieDetails: null,
genreOptions: [],
};
I want to pass the genreOptions here to genres then map over it in the MovieResults component.
<MovieResults>
{totalCount > 0 && <TotalCounter>{totalCount} results</TotalCounter>}
<MovieList movies={results || []} genres={genreOptions || []} />
</MovieResults>
Why can't I? Any ideas? I have done it for another similar request :S
UPDATE TO SHOW MOVIELIST COMPONENT
export default class MovieList extends React.Component {
render() {
const { movies, genres } = this.props;
const testFunction = (movieGenreIds) => {
const matchMovieGenresAndGenreIds = genres.map((genreId) => {
const matchedGenres = movieGenreIds.find((movieGenre) => {
return movieGenre.id === genreId
})
return matchedGenres // this returns the matching objects
})
const result = matchMovieGenresAndGenreIds.filter(Boolean).map((el)=> {
return el.name
})
return result
}
return (
<MoviesWrapper>
{movies.map((movie) => {
const {
title,
vote_average,
overview,
release_date,
poster_path,
genre_ids
} = movie;
return (
<MovieItem
title={title}
rating={vote_average}
overview={overview}
release={release_date}
poster={poster_path}
movieGenres={testFunction(genre_ids)}
/>
);
})}
</MoviesWrapper>
);
}
}
**** MOVIE ITEM COMPONENT***
export default class MovieItem extends React.Component {
render() {
const { title, overview, rating, release, poster, movieGenres } = this.props;
return (
// The MovieItemWrapper must be linked to the movie details popup
<MovieItemWrapper>
<LeftCont>
<img
className="movie-img"
src={`https://image.tmdb.org/t/p/w500${poster}`}
/>
</LeftCont>
<RightCont>
<div className="movie-title-container">
<h2 className="movie-title">{title}</h2>
<Rating>{rating}</Rating>
</div>
<div>{movieGenres}</div>
<p>{overview}</p>
<p>{release}</p>
</RightCont>
</MovieItemWrapper>
);
}
}
Please follow this steps to fix your code. I'll try yo explain what's happening along the way:
In your main component. Set the state to the value that you really want to pass to your child component. Remember that response will be an array of objects.
componentDidMount() {
this.getGenreList().then((response) => {
this.setState({genreOptions: response});
});
}
In your MovieList component. Please check your testFunction to respect data types. The following code will return you an array of strings containing the names of the genres that are included in the movies genres array.
const testFunction = (movieGenreIds) => {
return genres
.filter((genre) => {
return movieGenreIds.includes(genre.id);
})
.map((genre) => genre.name);
};
In your MovieItem component. (This is were the real problem was)
Instead of:
<div>{movieGenres}</div>
You may want to do something like this:
<div>{movieGenres.join(' ')}</div>
This converts your array into a string that can be rendered. Your error was due to the fact that you were passing there an array of objects that React couldn't render.
If you have any doubt, please let me know.
NOTE: I suggest you to use a type checker to avoid this kind of problems. And to be consistent with your variables naming conventions.
Update based on new information from chat:
In your ExpandableFilters component, you must fix the following piece of code to get the genre name (string). As explained in chat, you can't have objects as a result for a JSX expression ({}), but only primitives that can be coerced to strings, JSX elements or an array of JSX elements.
<GenreFilterCont marginTop>
{filtersShown && (
<ExpandableFiltersUl>
{this.props.movieGenres.map((genre, index) => {
return (
<ExpandableFiltersLi key={index}>
<Checkbox />
{genre.name}
</ExpandableFiltersLi>
);
})}
</ExpandableFiltersUl>
)}
</GenreFilterCont>
Please also note that I've added a key property. You should do it whenever you have a list of elements to render. For more about this I will refer you to the React Docs.

Categories