Next.js: How do you pass data to a route created dynamically - reactjs

I have a component that is receiving data:
const ProductTile = ({ data }) => {
let { productList } = data
var [products] = productList
var { products } = products;
return (
<div>
<div className="p-10 grid grid-cols-1 sm:grid-cols-1 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-3 gap-5">
{products.reduce((products, product) => products.find(x => x.productId === product.productId) ? products : [...products, product], []).map(({ colorCode, defaultColorCode, now, productId, productCode, productDescription, }, index) => {
return (
<Link key={`${productId}${index}`}
href={{
pathname: '/s7-img-facade/[slug]',
query: { slug: productCode },
}}
passHref>
/* template */
</Link>
)
})}
</div>
</div>
)
}
export default ProductTile
It creates a grid of templates each wrapped in a <Link> component which is rendering a dynamic component;
/s7-img-facade/[product]
What I would like is for the dynamic component to have access to products object which is in the ProductTile .
I know that I can do a getStaticProps in the dynamic component to do another request but that seems redundant and not dry...
Any ideas how the dynamic component get access to the products object?
Thanks in advance!

You've got the right idea - you can pass additional properties in the query field, but you'll need to use getServerSideProps to extract those from the query param and pass it to the page component as props. Something like this:
// pages/product.js
...
<Link key={`${productId}${index}`}
href={{
pathname: '/s7-img-facade/[slug]',
query: {
description: productDescription,
slug: productCode
},
}}
passHref
>
/* template */
</Link>
...
// pages/s7-img-facase/[slug].js
export default function S7ImgFacasePage({ description }) {
return <p>{ description }</p>
}
export const getServerSideProps = ({ params }) => {
const description = { params }
return {
props: {
description
}
}
}
So basically you pass it from the first page in params, read that in getServerSideProps of the second page, and then pass that as a prop to the second page component.
You mentioned getStaticProps - this won't work with static pages because getStaticProps is only run at build time so it won't know anything about the params you send at run time. If you need a fully static site, then you should consider passing it as a url parameter and reading it in useEffect or passing all possible pages through getStaticPaths and getStaticProps to generate all your static pages.

Related

Why does dynamic page shows the same data per request in Next.js?

I have a dynamic page named [className].js. In it, I'm trying the fetch the data using getServerSideProps. Currently, I have only two sets of data. When I click a link, that's supposed to take me the dynamic page with the className and show the data within it. But, no matter which link I click, it always shows the data of the first one.
Why is this happening? How can I get the data of the specified className?
export default function classPage({ classDetail }) {
const capitalizedClassTitle =
classDetail.title.charAt(0).toUpperCase() + classDetail.title.slice(1);
const classTitle = capitalizedClassTitle.replace(/-/g, " ");
return (
<div>
<NavBar />
<div>This is the page for {classTitle}</div>
<div className="w-20 h-20">
<Image
src={classDetail.classImageURL}
width={classDetail.imageWidth}
height={classDetail.imageHeight}
/>
</div>
</div>
);
}
export async function getServerSideProps({ query: { className } }) {
const res = await fetch(
`http://localhost:3000/api/admin/classes/${className}`
);
const { data } = await res.json();
return { props: { classDetail: data } };
}
I found the solution. In the api pages of [className], I had this line of code wrong.
const classDetail = await Class.findOne({className});
I changed it to await Class.findOne({title: className}) and now it is working as I wished. title was the keyword for className in my Class schema.

Confusion about handling graphQL data in a Nextjs project

I'm building a webpage with Next.js and graphQL, using Apollo as a client.
On on the main page (index.js) I want a "capsules" and "past launches" link. When the user clicks on either of these links, it directs them to a page detailing either the capsules or the past launches, where mission names are displayed. This details page is controlled by [parameters].js
The problem is this: I want render either the name of the capsule or the launch, depending on what the user has clicked on. However, in the data returned from the API call, the name properties are described using different keys: for capsules the key is name and past launches is mission_name. API data as follows:
{
"data": {
"capsules": [
{
"landings": 1,
"missions": [
{
"flight": 14,
"name": "CRS-3"
}
]
}
],
"launchesPast": [
{
"mission_name": "Starlink-15 (v1.0)",
"id": "109"
}
]
}
}
This presents an issue, because when I'm mapping through the returned data in [parameters].js I can only use either name or mission_name to select and render the name property - which isn't useful since I want to dynamically render the name of whatever element the user clicks on.
What's the best way to handle my API data/structure my project so I can render the relevant data, despite different key names?
Here's my index page - please ignore the other links:
index.js
import { useContext } from 'react'
import { LaunchContext } from '../spacexContext'
import Link from 'next/link'
import Card from '../components/Card'
export const Home = () => {
const data= useContext(LaunchContext)
return (
<div>
<Link href = {`/launchesPast`}>
<div>{data.launchesPast.length} launches last year</div>
</Link>
<Link href = {`/launchesUpcoming`}>
<div> Upcoming launches</div>
</Link>
<Link href = {`/missions`}>
<div>Missions</div>
</Link>
<Link href = {`/payloads`}>
<div> Payloads</div>
</Link>
<Link href = {`/users`}>
<div>Users</div>
</Link>
<Link href = {`/coresPast`}>
<div>Used cores to date</div>
</Link>
</div>
)
}
export default Home
[parameters].js
import { useRouter } from 'next/router'
import { useContext,useState,useEffect } from 'react'
import { LaunchContext } from '../spacexContext'
const Items = () => {
const [path, setPath] = useState("");
const data = useContext(LaunchContext);
const router = useRouter();
useEffect(() => {
if (router.isReady) {
setPath(router.query.parameter);
}
}, [router.isReady]);
return (
<div>
{data &&
path &&
data[path].map((item, i) => {
return (
<>
<p>{item.mission_name}</p>
<p>{item.launch_date_local}</p>
</>
)
})}
</div>
);
};
export default Items;
It occured to me to have some sort of conditional in [paramter].js as follows:
let name
if(item.mission_name){
name = mission_name
} else if (item.name) {
name = item.name
}
...however, this feels like a problematic solutution.

There's not a page yet at /second%20post

I have an issue regarding my gatsby site. I am fetching content from contentful and according to the code in my gatsby-node.js it has to generate two pages and it does but only one of them is working when I click on it for the second one it show that
There's not a page yet at /second%20post
I am so confused cause most of the issues asked here telling that they are not able to generate the pages and I don't know if the page is created or not and if it is created then why it shows me the error message and also when got to the error page the
second post
link is given but it is non clickable. all the other code is in my git repository here at Github code
Pleas refer to image for clear understanding at
Image here
here is my gatsby-node.js code file
const path = require(`path`)
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions;
// Define a template for blog post
const blogPost = path.resolve(`./src/templates/blog-post-contentful.js`)
// Get all markdown blog posts sorted by date
const result = await graphql(
`
{
allContentfulBlockchainlearning{
edges{
node{
slug
title
subtitle
}
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading your blog posts`,
result.errors
)
return
}
const posts = result.data.allContentfulBlockchainlearning.edges
// Create blog posts pages
// But only if there's at least one markdown file found at "content/blog" (defined in gatsby-
config.js)
// `context` is available in the template as a prop and as a variable in GraphQL
if (posts.length > 0) {
posts.forEach((post, index) => {
const previousPostSlug = index === 0 ? null : posts[index - 1].id
const $nextPostSlug = index === posts.length - 1 ? null : posts[index + 1].id
createPage({
path: post.node.slug,
component: blogPost,
context: {
slug: post.node.slug,
previousPostSlug,
$nextPostSlug,
},
})
})
}
}
and here is my blog-post template I want to creat
import React from "react"
import { Link, graphql } from "gatsby"
import Bio from "../components/bio"
import Layout from "../components/layout"
import SEO from "../components/seo"
const BlogPostTemplate = ({ data, location }) => {
const post = data.contentfulBlockchainlearning
const siteTitle = data.site.siteMetadata?.title || `Title`
const { previous, next } = data
return (
<Layout location={location} title={siteTitle}>
<SEO
title={post.title}
description={post.subtitle}
/>
<article
className="blog-post"
itemScope
itemType="http://schema.org/Article"
>
<header>
<h1 itemProp="headline">{post.title}</h1>
<p>{post.date}</p>
</header>
<section
dangerouslySetInnerHTML={{ __html: post.content.raw }}
itemProp="articleBody"
/>
<hr />
<footer>
<Bio />
</footer>
</article>
<nav className="blog-post-nav">
<ul
style={{
display: `flex`,
flexWrap: `wrap`,
justifyContent: `space-between`,
listStyle: `none`,
padding: 0,
}}
>
<li>
{previous && (
<Link to={previous.slug} rel="prev">Hey There
← {previous.title}
</Link>
)}
</li>
<li>
{next && (
<Link to={next.slug} rel="next">
{next.title} →
</Link>
)}
</li>
</ul>
</nav>
</Layout>
)
}
export default BlogPostTemplate
export const pageQuery = graphql`
query BlogPostBySlug(
$slug: String!
$previousPostSlug: String
$nextPostSlug: String
) {
site {
siteMetadata {
title
}
}
contentfulBlockchainlearning(slug: {eq: $slug}){
title
subtitle
content{
raw
}
}
previous: contentfulBlockchainlearning(slug: { eq: $previousPostSlug}) {
title
}
next: contentfulBlockchainlearning(slug: { eq: $nextPostSlug }) {
title
}
}
`
The issue is simple, you can't create a URL with whitespace like the one you are trying to create. second page should be parsed as second-page since the whitespace between second and page potentially will cause a lot of issues.
Gatsby is creating properly the pages since they appear on the 404 page (under gatsby develop, the 404 page lists all you created pages). However, it doesn't have a valid route because your slugs must be slugified. Ideally, the slug should be fetched with the correct format from the CMS already, however, you can add some controls to avoid this behaviour:
if (posts) {
posts.forEach((post, index) => {
let slugifiedPath= post.node.slug.toLowerCase().replace(/\s/g, '-');
const previousPostSlug = index === 0 ? null : posts[index - 1].id
const $nextPostSlug = index === posts.length - 1 ? null : posts[index + 1].id
createPage({
path: slugifiedPath,
component: blogPost,
context: {
slug: post.node.slug,
previousPostSlug,
$nextPostSlug,
},
})
})
}
It's quite self-explanatory but, since your paths are being fetched with a wrong format, you need to refactor them by:
let slugifiedPath= post.node.slug.toLowerCase().replace(/\s/g, '-');
It transforms it to lower case and it replaces all-white spaces globally (/\s/g) using a regular expression for hyphens (-), creating a valid slug.

Getting data Gatsby.js and contentful

I have pages in contenful with different URLs. Now I'm getting all data from all pages, but I need to get different data for different URL. I'm tryin to filter it, but get error. So How I can check if url='something' I need query it ?
import React from "react";
import { StaticQuery, graphql } from "gatsby";
import ArticleMfo from "../components/articleMfo";
const Products = () => (
<StaticQuery
query={graphql`
query MyQuery {
allContentfulAllPages(filter: {link: {eq: $MYURL}}) {
edges {
node {
mfo {
__typename
... on ContentfulBank {
id
text
limit
rate
term
link
logo {
title
file {
url
}
}
}
}
}
}
}
}
`}
render={data => (
<Container className="container">
{data.allContentfulAllPages.edges.map(({ node }, i) => (
<div>
{node.mfo.map(mfos => (
<ArticleMfo key={mfos.id} content={mfos} />
))}
</div>
))}
</Container>
)}
/>
);
export default Products
Static query (hence the name) does not accept variables. As you can see from the Static Query docs:
StaticQuery does not accept variables (hence the name “static”), but
can be used in any component, including pages
If you want to filter it, you will need to use a page query and pass the variable name (MYURL) via context on each page. In that case, you'll need to move your query to gatsby-node.js and, on every page creation, pass the variable through context to make it available to use as a filter. Something like:
const path = require("path")
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const result = await graphql(
`
{
allMarkdownRemark(limit: 1000) {
edges {
node {
frontmatter {
path
}
}
}
}
}
`
)
// Handle errors
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
const blogPostTemplate = path.resolve(`src/templates/blog-post.js`)
result.data.allMarkdownRemark.edges.forEach(({ node }) => {
const path = node.frontmatter.path
createPage({
path,
component: blogPostTemplate,
// In your blog post template's graphql query, you can use pagePath
// as a GraphQL variable to query for data from the markdown file.
context: {
pagePath: path,
},
})
})
}
Note: Replace the query above and the resolvers for your data.
With the snippet above, every page created from the GraphQL query will have the path available (as pagePath) through context to filter, adapt it to your needs.

How to dynamically set variables in mutation with react-adopt?

In my component, I am using react-adopt, to compose graphql queries and mutations, so that my render props don't get too messy. I have the following code:
This is my mutation, it takes one argument - planID.
const CREATE_ORDER_MUTATION = gql`
mutation CREATE_ORDER_MUTATION($planID: String!) {
createOrder(planID: $planID) {
planID
name
description
subscriptionType
images
subscriptionType
pricePerMonth
}
}
This is the adopt function, it takes a couple of mutations, one of which is createOrder. The way Apollo works is that I need to pass variables prop to createOrder component here. The problem is, I don't have the planID at this point. planID is available only inside of the actual component.
const Composed = adopt({
toggleCart: <Mutation mutation={TOGGLE_CART_MUTATION} />,
createOrder: <Mutation mutation={CREATE_ORDER_MUTATION} />,
});
My component looks like this. I have the planID available here, but how can I pass it as an argument to mutation?!
render() {
const { plan } = this.props;
return (
<Composed>
{({ toggleCart, createOrder }) => {
const handleAdd = () => {
toggleCart();
Router.push('/waschingmachine');
createOrder();
};
return (
<StyledPlan>
<h1>{plan.name}</h1>
<p>{plan.description}</p>
<img src={`static${plan.images[0]}`} alt="blue1" />
<h2>{plan.pricePerMonth / 100} EUR</h2>
<div className="buttons">
<Link
href={{
pathname: '/plan',
query: { id: plan.id },
}}
>
<button type="button">info</button>
</Link>
<button onClick={handleAdd} type="button">
Select Plan
</button>
</div>
</StyledPlan>
);
}}
</Composed>
);
}
If there is no way to solve it this way, how would you approach it differently?
Here is how you can pass arguments through react-adopt way into your inner mutations mapper:
// In a nutshell, `react-adopt` allows you to pass props to your `Composed`
// component, so your inner `mapper` can access those props to pass them to
// your <Query> or <Mutation>
// Here's your initial `Composed` component from above
const Composed = adopt({
// `planId` is passed from below via props (see `<ContainerComponent />)
toggleCart: ({ planId, render }) => (
<Mutation mutation={TOGGLE_CART_MUTATION} variables={{ planId }}>{render}</Mutation>,
),
// `planId` is passed from below via props (see `<ContainerComponent />)
createOrder: ({ planId, render })=> (
<Mutation mutation={CREATE_ORDER_MUTATION} variables={{ planId }}>{render}</Mutation>
)
});
// `<ContainerComponent />` will take a plan as its props and passed `planId` to
// the `<Composed />` component
const ContainerComponent = ({ plan }) => (
<Composed planId={plan.id}>
{({ toggleCart, createOrder }) => {
const handleAdd = e => {
e.preventDefault();
toggleCart();
// ...
createOrder();
// ...
};
// Whatever your UI needs you can pass them here via here
return <YourPresentationComponent onClick={handleAdd} />;
}}
</Composed>
)
// Now all you need to do is to pass `plan` from your render() to the
// <ContainerComponent /> (This is the render() where you return your
render() {
const { plan } = this.props;
return <ContainerComponent plan={plan} />
}
Hopefully this can be helpful to solve your issue! Just a side note, you can also get the previous mapper value and pass them onto the next mapper fn as part of the argument, see below:
const Composed = adopt({
// Here you can retrieve the actual `plan` by using `userId` and pass the result
// into your next children mapper fn to consume
plan: ({ userId, render }) => (
<Query query={GET_PLAN_GRQPHQL} variables={{ userId }}>{render}</Query>
),
// `plan` is actually coming from above <Query /> component
toggleCart: ({ plan, render }) => { //... },
createOrder: ({ plan, render }) => { //...}
});
The mutate function passed in the rendered children can be called with options.
Options can include variables used in the GraphQL mutation string. [1].
This means that you can call the createOrder mutation function like so.
createOrder({ variables: { planID: 'some plan id' } });
Given the dynamic nature of planID, there are a number of ways to implement this. One of which is to use data attributes as below:
A data attribute can be set on for the plan id on the button .
<button onClick={handleAdd} data-planid={plan.id} type="button">
Select Plan
</button>
handleAdd can be refactored to get the planid from the target dataset attribute and invoke createOrder with planID variable.
const handleAdd = event => {
const planID = event.target.dataset.planid;
toggleCart();
Router.push('/waschingmachine');
createOrder({ variables: { planID } });
};
Another is to directly pass planID to handleAdd when calling it in the onClick prop for the button.
<button onClick={() => handleAdd(plan.id)} type="button">
Select Plan
</button>
Then update the handler
const handleAdd = planID => {
toggleCart();
Router.push('/waschingmachine');
createOrder({ variables: { planID } });
};
There are tradeoffs to both approaches. For the earlier approach, the planid are set in the DOM as attributes and can be read later.
While for the later one, N handlers are created for N plans and are kept in memory.

Resources