I'm using Next.js static site generation without fetching data in getStaticProps. Also using tailwind with clsx utility function.
Here is the relevant component:
export const Sidebar = (props: SidebarProps) => {
const { feedbacks } = useContext(FeedbackContext)
// returns [{status: 'planned', feedbackCount: 2}, {status: 'live', feedbackCount: 1}, ...]
const roadmapStatusCount = getRoadmapStatusCount(feedbacks)
// ...
I'm rendering featureCountElements based on roadmapStatusCount:
const featureCountElements = roadmapStatusCount.map(
({ status, feedbackCount }) => {
return (
<div key={status}>
<span
className={clsx(
"inline-block h-2 w-2 rounded-full",
status === "planned" && "bg-accentSecondary",
status === "in-progress" && "bg-accentPrimary-800",
status === "live" && "bg-primary-400"
)}
></span>
<dt className="grow capitalize text-secondary-700">{status}</dt>
<dd className="font-bold text-secondary-800">{feedbackCount}</dd>
</div>
)
}
)
The error says: Warning: Prop className did not match. Server: inline-block h-2 w-2 rounded-full bg-primary-400 Client: inline-block h-2 w-2 rounded-full bg-accentPrimary-800. Background value changed. This means that for some reason one of the status values was different or perhaps elements rendered in different order?
The context looks like this. The initial value productRequests comes from a local, static JSON file.
interface IFeedbackContext {
feedbacks: Feedback[]
setFeedbacks: Dispatch<SetStateAction<Feedback[]>> | null
}
export const FeedbackContext = createContext<IFeedbackContext>({
feedbacks: productRequests,
setFeedbacks: null
})
I'm uncertain about server side vs client side rendering in this case. What is the issue here? This wasn't a problem until I introduced useContext hook.
Related
I am taking a React course in which week by week we have certain challenges that lead us to create an E-Commerce.
My problem is that, I have the data of a product hardcoded, when entering the page useEffect creates a promise that is resolved in 2 seconds using setTimeOut and returns the product data.
In a previous challenge I already did essentially the same thing, only having an array with several products and following the design pattern of: ItemListContainer asks for the data, passes the data to ItemList, applies .map() to the array and for each item creates an Item component sending by props the data.
In the current challenge as we are working with a single product we do not have to do .map() on any array, and this for some reason causes the Item component (in this case called ItemDetail) to render before the data arrives, although it only renders the parts that do not depend on the data arriving to it.
Demo: Demo (It renders the styled div and the "$" sign).
After several hours looking at the code I can't figure out why it happens and what I could do to fix it.
Github repo: enzom-uy/coderhouseECommerce
ItemDetailContainer code:
import React, { useState, useEffect } from 'react'
import ItemDetail from '../ItemDetail/ItemDetail'
const productData = {
id: '4',
name: 'Cuarto producto',
description: 'Descripcion del cuarto producto',
price: 10,
pictureUrl:
'https://media.istockphoto.com/vectors/thumbnail-image-vector-graphic-vector-id1147544807?k=20&m=1147544807&s=612x612&w=0&h=pBhz1dkwsCMq37Udtp9sfxbjaMl27JUapoyYpQm0anc='
}
const ItemDetailContainer = () => {
const [detailedProduct, setDetailedProduct] = useState({})
useEffect(() => {
const fetchingData = new Promise((res, rej) => {
setTimeout(() => {
res(productData)
}, 2000)
})
fetchingData.then((res) => {
setDetailedProduct(res)
console.log('Se guardaron los datos')
})
fetchingData.catch((err) => {
console.log('Failed')
})
}, [])
return (
<>
<h1>Producto detallado</h1>
<ItemDetail product={detailedProduct} />
</>
)
}
export default ItemDetailContainer
ItemDetail code:
import React from 'react'
export default function ItemDetail({ product }) {
return (
<div className="bg-slate-100 w-60 flex flex-col items-center mx-1 px-2 border border-slate-400 text-center">
<span>{product.name}</span>
<img src={product.pictureUrl} width="120" />
<p>{product.description}</p>
<span>${product.price}</span>
</div>
)
}
The Short Answer
It is showing the styled div and the dollar sign because that is what you have included in the JSX. To remove it until the data arrives, you can make it a part of the return statement conditionally on the data being present. That might look something like this (using the simpler of the options listed later in my answer)
return (
<>
<h1>Producto detallado</h1>
{detailedProduct.name && <ItemDetail product={detailedProduct} />}
</>
)
Why It's Happening
If you have a simple component that doesn't rely on data, then it will render everything in the return statement. In the below example, that is the div, h1, and p element.
function Parent() {
return (
<div>
<h1>Hello!</h1>
<p>We have no products to sell.</p>
</div>
);
}
If you have a component that does rely on data, say from an API (or a setTimeout), then that doesn't change. What is included in the return statement (including any child components) will be rendered.
function Parent() {
const [data, setData] = useState({});
useEffect(() => {
fetch('https://api.com')
.then(res => res.json())
.then(setData)
.catch(err => console.error(err));
}, []);
return (
<div>
<h1>Hello!</h1>
<Child data={data} />
</div>
);
}
function Child({ data }) {
return (
<p>The name is: {data.name}</p>
);
}
In this case, before the data comes back from the API, the child element's <p> element with the text "The name is: " will appear, since it is a part of the return. Then, when the data arrives, it will re-render and the name will appear. If we want it to say something else, we can do that with something like this...
function Child({ data }) {
return (
<p>{data.name ? `The name is: ${data.name}` : "Loading name..."}</p>
);
}
This will ask the component, "is there a value for the data.name property? If yes, then display the text "The name is: ___" where the name is filled in the blank. If no, then display the text "Loading name..."
This is also option 1 for your solution. You can use a ternary operator to provide a default value when the data has not yet arrived.
Another option is to just remove the entire component that relies on the data. Since the return statement will only return items that are truth-y, you can conditionally render your ItemDetail component. Our example might look something like this...
function Parent() {
const [data, setData] = useState({});
useEffect(() => {
fetch('https://api.com')
.then(res => res.json())
.then(setData)
.catch(err => console.error(err));
}, []);
return (
<div>
<h1>Hello!</h1>
{data.name && <Child data={data} />}
</div>
);
}
function Child({ data }) {
return (
<p>The name is: {data.name}</p>
);
}
This is option 2 for your solution. It is quick and easy, but there are distinct differences between how they impact developer and user experience.
As an option 3, you could also implement a loading component that would take the place of the product component (including something like a spinner or progress bar), but I think it may be more work than your React course calls for.
Important Note. While the second option is definitely easier since you don't have to code default values for every field, it can sometimes be worse for user experience, since entire components will appear and disappear when data loads in, which can cause big layout shifts.
You can do a few things like:
Don't render your component until the request is done or...
Use loading flag state
Use an extra state for the "loading" flag.. you can start this on "true" and after finished change it to false and render your component with the data.
This is my example:
import React, { useState, useEffect } from "react";
const productData = {
id: "4",
name: "Cuarto producto",
description: "Descripcion del cuarto producto",
price: 10,
pictureUrl:
"https://media.istockphoto.com/vectors/thumbnail-image-vector-graphic-vector-id1147544807?k=20&m=1147544807&s=612x612&w=0&h=pBhz1dkwsCMq37Udtp9sfxbjaMl27JUapoyYpQm0anc="
};
const ItemDetailContainer = () => {
const [detailedProduct, setDetailedProduct] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchingData = new Promise((res, rej) => {
setTimeout(() => {
res(productData);
}, 2000);
});
fetchingData.then((res) => {
setDetailedProduct(res);
setLoading(false);
console.log("Se guardaron los datos");
});
fetchingData.catch((err) => {
console.log("Failed");
});
}, []);
if (loading) return "Cargando...";
return (
<>
<h1>Producto detallado</h1>
<ItemDetail product={detailedProduct} />
</>
);
};
export default ItemDetailContainer;
function ItemDetail({ product }) {
return (
<div className="bg-slate-100 w-60 flex flex-col items-center mx-1 px-2 border border-slate-400 text-center">
<span>{product.name}</span>
<img src={product.pictureUrl} width="120" />
<p>{product.description}</p>
<span>${product.price}</span>
</div>
);
}
I'm new to React and am trying to animate some elements from an array using the ReShake.js library. Here's my code:
import { ShakeHorizontal } from 'reshake'
const state = {
invalidWordSubmitted: true,
}
const isInvalidWordSubmitted = () => {
return state.invalidWordSubmitted ? '.invalidWordSubmitted' : ''
}
// here is the bit that I think is causing the problem:
<div id="gameBoard" className="gameBoard">
{board.map((row, rowNumber) =>
row.map((letter, colNumber) => (
<span
key={colNumber}
>
<ShakeHorizontal trigger={`${isInvalidWordSubmitted()}`}>
{letter}
</ShakeHorizontal>
</span>
))
)}
</div>
It works ok, but there is serious lag in the browser, and the console shows me the following warning: react_devtools_backend.js:4045 Over 200 classes were generated for component styled.div with the id of "sc-bdvvtL"
Should I be adding unique ids to each of the iterated <ShakeHorizontal> components?
I couldn't find anything in the docs: https://github.com/elrumordelaluz/reshake/tree/master/src
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.
I'm developing a blog on next.js with sanity.io, and I'm having trouble using the code-input plugin.
What I do have
I'm able to use the code component block on sanity, which looks something like this:
Everything good on the sanity side. My problem comes with using it on the next.js [slug].js file.
I have this error prompt out:
This issue with this is that I don't have a serializer.js file/component anywhere on my code, not even on the studio root folder. I've seen this applies for gatsby but I don't know how to apply it for Next.js
This is what I currently Have:
import groq from 'groq'
import imageUrlBuilder from '#sanity/image-url'
import BlockContent from '#sanity/block-content-to-react'
import client from '../../client'
import Layout from '../../components/layout'
import utilStyles from '../../styles/utils.module.css'
import styles from '../../components/layout.module.css'
function urlFor (source) {
return imageUrlBuilder(client).image(source)
}
const Post = (props) => {
const {
title = 'Missing title',
name = 'Missing name',
categories,
authorImage,
mainImage,
code,
body = []
} = props
console.log(props)
return (
<Layout>
<article>
<div className={styles.container}>
<figure>
<img src={urlFor(mainImage).url()} />
</figure>
<h1 className={utilStyles.headingXl}>{title}</h1>
{categories && (
<ul className="inline">
Category:
{categories.map(category =>
<li key={category}>
<span className="inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-indigo-100 bg-indigo-700 rounded">{category}</span>
</li>)}
</ul>
)}
<BlockContent
blocks={body}
imageOptions={{fit: 'max'}}
{...client.config()}
{...code}
/>
</div>
</article>
</Layout>
)
}
const query = groq ` *[_type == "post" && slug.current == $slug][0]{
title,
"name": author->name,
"categories": categories[]->title,
mainImage,
code,
"authorImage": author->image,
body,
}`
Post.getInitialProps = async function(context) {
const {slug = ""} = context.query
return await client.fetch(query, { slug })
}
export default Post
I really would appreciate some help here! Thanks <3
You can pass a serializer for the code block type to your BlockContent using the serializers prop.
const serializers = {
types: {
code: props => (
<pre data-language={props.node.language}>
<code>{props.node.code}</code>
</pre>
)
}
}
// ...
<BlockContent
blocks={body}
imageOptions={{fit: 'max'}}
{...client.config()}
{...code}
serializers={serializers}
/>
Summary / Issue
I've created an anime database app using Nextjs with deployment on Vercel. The build was fine and the initial page rendered, but only a few of my dynamic routes are being rendered, the rest display a 404 page. I went into the deploy log and found that for each dynamic page, only 10 routes were being built for every dynmaic route.
Deploy Screenshot from Vercel
While working in development (localhost:3000), there were no issues and everything ran fine.
The routes are based on the id of each title and there are thousands of titles.
My Code
Here is my code for one of the pages using getStaticPaths and getStaticProps
export const getStaticProps = async ({ params }) => {
const [anime, animeCharacters, categories, streaming, reviews] = await Promise.all([
fetch(`https://kitsu.io/api/edge/anime/${params.id}`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/characters`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/categories`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/streaming-links`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/reviews`),
])
.then((responses) =>
Promise.all(responses.map((response) => response.json()))
)
.catch((e) => console.log(e, "There was an error retrieving the data"))
return { props: { anime, animeCharacters, categories, streaming, reviews } }
}
export const getStaticPaths = async () => {
const res = await fetch("https://kitsu.io/api/edge/anime")
const anime = await res.json()
const paths = anime.data.map((show) => ({
params: { id: show.id },
}))
return { paths, fallback: false }
}
[id] is my dynamic route and as you can see, it's only being populated with 10 routes (the first 3 and 7 additional).
Despite the number of shows, I'm looping over each show and grabbing its ID and then passing that as the path.
What I've thought of
The API I'm using is the Kitsu API.
Within the docs, it states: "Resources are paginated in groups of 10 by default and can be increased to a maximum of 20". I figured this might be why 10 paths are being generated, but if that was the case, then why would it work fine in production and in deployment? Also, when I click on each poster image, it should bring me to that specific title by its id, whihc is dynamic, so it shouldn't matter how many recourses are being generated initially.
Code for dynamic page `/anime/[id]
import { useState } from "react"
import { useRouter } from 'next/router'
import fetch from "isomorphic-unfetch"
import formatedDates from "./../../helpers/formatDates"
import Navbar from "../../components/Navbar"
import TrailerVideo from "../../components/TrailerVideo"
import Characters from "./../../components/Characters"
import Categories from "../../components/Categories"
import Streamers from "../../components/Streamers"
import Reviews from "../../components/Reviews"
const Post = ({ anime, animeCharacters, categories, streaming, reviews}) => {
const [readMore, setReadMore] = useState(false)
const handleReadMore = () => setReadMore((prevState) => !prevState)
let {
titles: { en, ja_jp },
synopsis,
startDate,
endDate,
ageRating,
ageRatingGuide,
averageRating,
episodeCount,
posterImage: { small },
coverImage,
youtubeVideoId,
} = anime.data.attributes
const defaultImg = "/cover-img-default.jpg"
const synopsisSubString = () =>
!readMore ? synopsis.substring(0, 240) : synopsis.substring(0, 2000)
const router = useRouter()
if(router.isFallback) return <div>loading...</div>
return (
<div className='relative'>
<div className='z-0'>
<img
className='absolute mb-4 h-12 min-h-230 w-full object-cover opacity-50'
src={!coverImage ? defaultImg : coverImage.large}
/>
</div>
<div className='relative container z-50'>
<Navbar />
<div className='mt-16 flex flex-wrap md:flex-no-wrap'>
{/* Main */}
<div className='md:max-w-284'>
<img className='z-50 mb-6' src={small} />
<div className='xl:text-lg pb-6'>
<h1 className='mb-2'>Anime Details</h1>
<ul>
<li>
<span className='font-bold'>Japanese Title:</span> {ja_jp}
</li>
<li>
<span className='font-bold'>Aired:</span>{" "}
{formatedDates(startDate, endDate)}
</li>
<li>
<span className='font-bold'>Rating:</span> {ageRating} /{" "}
{ageRatingGuide}
</li>
<li>
<span className='font-bold'>Episodes:</span> {episodeCount}
</li>
</ul>
</div>
<Streamers streaming={streaming} />
</div>
{/* Info Section */}
<div className='flex flex-wrap lg:flex-no-wrap md:flex-1 '>
<div className='mt-6 md:mt-40 md:ml-6 lg:mr-10'>
<h1 className='sm:text-3xl pb-1'>{en}</h1>
<h2 className='sm:text-xl lg:text-2xl pb-4 text-yellow-600'>
{averageRating}{" "}
<span className='text-white text-base lg:text-lg'>
Community Rating
</span>
</h2>
<div>
<p className='max-w-2xl pb-3 overflow-hidden xl:text-lg'>
{synopsisSubString()}
<span className={!readMore ? "inline" : "hidden"}>...</span>
</p>
<button
className='text-teal-500 hover:text-teal-900 transition ease-in-out duration-500 focus:outline-none focus:shadow-outline'
onClick={handleReadMore}
>
{!readMore ? "Read More" : "Read Less"}
</button>
</div>
<Categories categories={categories} />
<Reviews reviews={reviews}/>
</div>
{/* Sidebar */}
<section className='lg:max-w-sm mt-10 md:ml-6 lg:ml-0'>
<TrailerVideo youtubeVideoId={youtubeVideoId} />
<Characters animeCharacters={animeCharacters} />
</section>
</div>
</div>
</div>
</div>
)
}
export const getStaticProps = async ({ params }) => {
const [anime, animeCharacters, categories, streaming, reviews] = await Promise.all([
fetch(`https://kitsu.io/api/edge/anime/${params.id}`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/characters`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/categories`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/streaming-links`),
fetch(`https://kitsu.io/api/edge/anime/${params.id}/reviews`),
])
.then((responses) =>
Promise.all(responses.map((response) => response.json()))
)
.catch((e) => console.log(e, "There was an error retrieving the data"))
return { props: { anime, animeCharacters, categories, streaming, reviews } }
}
export const getStaticPaths = async () => {
const res = await fetch("https://kitsu.io/api/edge/anime")
const anime = await res.json()
const paths = anime.data.map((show) => ({
params: { id: show.id },
}))
return { paths, fallback: true }
}
export default Post
Screenshot of Errror
Repo
If the API you are working with serves resources in groups of 10, then when you call the API in getStaticPaths you only have 10 id ahead of time. Using static generation in nextjs builds static pages for all the available ids ahead of time in production mode. But when in development mode your server will recreate each page on per request basis. So to solve this problem you can build the first 10 pages and make the rest of the pages to be fallback. Here is how you do it.
export const getStaticPaths = async () => {
const res = await fetch("https://kitsu.io/api/edge/anime")
const anime = await res.json()
// you can make a series of calls to the API requesting
// the next page to get the desired amount of data (100 or 1000)
// how many ever static pages you want to build ahead of time
const paths = anime.data.map((show) => ({
params: { id: show.id },
}))
// this will generate 10(resource limit if you make 1 call because your API returns only 10 resources)
// pages ahead of time and rest of the pages will be fallback
return { paths, fallback: true }
}
Keep in mind when using {fallback: true} in getStaticPaths you need have some sort of loading indicator because the page will be statically generated when you make a request for the first time which will take some time(usually very fast).
In your page that you want to statically generate
function MyPage = (props) {
const router = useRouter()
if (router.isFallback) {
// your loading indicator
return <div>loading...</div>
}
return (
// the normal logic for your page
)
}
P.S. I forgot to mention how to handle errors where API responds with 404 or 500 and the resource doesn't exist to send as props when using fallback in static generation.
So here's how to do it.
const getStaticProps = async () => {
// your data fetching logic
// if fail
return {
props: {data: null, error: true, statusCode: 'the-status-code-you-want-to-send-(500 or 404)'}
}
// if success
return {
props: {data: 'my-fetched-data', error: false}
}
}
// in the page component
import ErrorPage from 'next/error';
function MyStaticPage(props) {
if (props.error) {
return <ErrorPage statusCode={404}/>
}
// else you normal page logic
}
Let me know if it helped or you encountered some error while implementing.
Here is where you can learn more https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation