Confusion about handling graphQL data in a Nextjs project - reactjs

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.

Related

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

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.

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.

How can I pass a function when linking with React Router?

In my React app I have a page which has a list of web items (WebItems.js) and on the page there is an Link which goes to a page for adding a new web item (Add.js).
const WebItems = () => (
async function getWebItems() {
const result = await httpGet('webitem/list');
if (result.items) {
setWebItems(result.items);
}
}
return <>
<Link
to="webitem/list"
Add
</Link>
</>
)
I need to pass the function getWebItems() to the Add component so that after a web item is added the list of web items gets updated.
I am more familiar with #reach/router although I need to use react-router-dom for this project.
I found this blog post but I am unsure if this is what I need.
Can anyone help?
Why don't you just make this function a custom hook and re-use it?
// useWebItems.js
import { useState } from 'react';
function useWebItems() {
const [webItems, setWebItems] = useState([]);
async function getWebItems() {
const result = await httpGet('webitem/list');
if (result.items) {
setWebItems(result.items);
}
}
return { getWebItems, webItems };
}
export default useWebItems;
// Add Component
import { getWebItems, webItems } from 'path/to/useWebItems.js';
// Do whatever you want with getWebItems, webItems
You could use route state and the object form of the to prop for the Link.
const WebItems = () => (
async function getWebItems() {
const result = await httpGet('webitem/list');
if (result.items) {
setWebItems(result.items);
}
}
return <>
<Link
to={{
pathname: "webitem/list",
state: {
getWebItems,
},
}}
>
Add
</Link>
</>
);
Use location from the route props on the receiving route/component
const { getWebItems } = props.location.state;
...
getWebItems();

Linking or Routing dynamicaly with Next.js

I am trying to find the best way to link to a details page from a list of objects that are mapped. The list fetches items from an API and works fine. The problem is that I cant pass the id to my details page and get this error when I click on the objects.
:3000/_next/static/development/pages/_app.js?ts=1592696161086:1299 GET http://localhost:3000/flowers/[object%20Object] 404 (Not Found)
and in the url http://localhost:3000/flowers/[object%20Object]
This is what I have in my /pages/flowers.js
import React, { useState, useEffect } from 'react'
import Head from 'next/head'
import Layout from '../components/layout'
import Link from 'next/link'
import utilStyles from '../styles/utils.module.css'
export default function Flowers() {
const LISTURL = 'https://flowers-mock-data.firebaseio.com/flowers.json'
const TESTURL = 'https://flowers-mock-data.firebaseio.com/flowers/9.json'
const [items, setItems] = useState([])
useEffect(() => {
fetch(LISTURL)
.then((res) => res.json())
.then((json) => setItems(json))
}, [] )
return (
<Layout>
<Head>
<title>🌸</title>
</Head>
<section className={utilStyles.centerSection}>
<button>Shade loving plants</button>
<button>Sun loving plants</button>
</section>
{items.map((item) => (
<ul key={item._id.oid}>
<li className={utilStyles.listItem}>
<Link href='/flowers/[flowerid]' as={`/flowers/${item._id}`}>
<a className={utilStyles.list}>
<h2 className={utilStyles.headingTop}>{item.common_name}</h2>
<img className={utilStyles.imgList} src={`${item.cover_image}`} alt={item.common_name} />
</a>
</Link>
</li>
</ul>
))
}
</Layout>
)
}
This is pages/[flowerid].js
import React, { useEffect, useState } from 'react'
import Head from 'next/head'
import Router from 'next/router'
import axios from 'axios'
import Layout from '../components/layout'
import utilStyles from '../styles/utils.module.css'
const FlowerDetail = () => {
const [flower, setFlower] = useState(null)
useEffect(() => {
const fetchData = async () => {
const { flowerId } = Router.query
try {
const { data } = await axios.get(
`https://flowers-mock-data.firebaseio.com/flowers/${flowerId}.json`
)
console.log(`blomma`, data)
setFlower(data)
} catch (error) {
console.log(`Can not find id`, error)
}
}
fetchData()
}, [])
if (!flower) return <div>Loading...</div>
console.log('no flowers to see')
return (
<Layout>
<Head>
<title>🌸</title>
</Head>
<div>
<p>Blooming season {flower.blooming_season}</p>
<img className={utilStyles.imgList} src={`${flower.cover_image}`} alt={flower.common_name} />
<p>Common name {flower.common_name}</p>
<h5>{flower.notes}</h5>
</div>
</Layout>
)
}
export default FlowerDetail;
The problems holding you back...
The _id coming from your API is an object which contains an oid property:
"_id": {
"oid": "5e57880c9fa50a095f475c18"
},
Therefore you'll want to use the items._id.oid property as a link source. As is, you're passing the id object to the link source, which is why your links are invalid: http://localhost:3000/flowers/[object%20Object]
Unfortunately, that's not the only problem. The other problem is that the current data structure doesn't contain any reference to the URL parameter id (where 1.json is supposed to reference oid: 5e579cdfe99bdd0ca1cf64a3, 2.json references oid: 5e579dfcb664120cd14b4331, and so on), so you'll need to reconfigure your API data to include a property with this URL id within each data document (id: 1, id: 2, ...etc) and then use that id as your link source OR you'll have to reconfigure your API to utilize this oid property as a URL parameter to match against (instead of 1.json it'll use the oid as a URL parameter to match against and be 5e579cdfe99bdd0ca1cf64a3.json).
For example, this API endpoint (which comes from http://jsonplaceholder.typicode.com) contains an id property that is used as a URL parameter to retrieve the specific JSON file by its id (notice how both the URL and the JSON data contain an id of 9).
On a side note, if you're using Chrome, you can install this extension to prettify/format JSON, so it looks like this.
OID doesn't match the requested API endpoint
For reference, here's how your response is currently structured when you query this endpoint (notice that the 9 in https://flowers-mock-data.firebaseio.com/flowers/9.json isn't referenced/used anywhere within the JSON data, so using the oid (https://flowers-mock-data.firebaseio.com/flowers/5e57a0e1e5f2470d789335c2.json) as a parameter won't be valid):
{
"__v": {
"numberInt": "0"
},
"_id": {
"oid": "5e57a0e1e5f2470d789335c2"
},
"blooming_season": "Late Spring",
"common_name": "Ornamental onion",
"cover_image": "https://images.unsplash.com/photo-1565685225009-fc85d9109c80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80",
"depth": {
"numberInt": "3"
},
"height": [
{
"numberInt": "6"
},
{
"numberInt": "60"
}
],
"latin_name": "Allium",
"notes": "Usually pest-free; a great cut flower",
"soil": [
"well drained",
"moist",
"fertile"
],
"spacing": {
"numberInt": "12"
},
"sun": true
}
Example fetch data repo
I've put together an example with some comments that incorporates the API endpoint (jsonplaceholder) mentioned in the paragraph above, as well as implementing some of nextjs's data fetching methods (excluded getStaticPaths for simplicity; however, technically, it can replace getServerSideProps in the dynamic pages/users/[id].js page since the data will never be changed/updated during runtime):
fetch data example repo

How to go back to the beginning of list that is being <Link> through

I have a list of lessons the I am pulling from a database. Each one renders a new page. I have Next and Prev buttons the through each of items passed through props. That works fine, the problem is when the end of the list is reached the app crashed, how do I loop back around to the first Item being rendered?
The prev and next buttons work fine. I tried including a condition in the button such as
{if(prev_id > 0 && next_id <= id.length){
//do something
}}
export default props => {
//These pull the the id from the backend and then I increment or decrement by 1 to link through the pages
const prev_id = Number(props.props.match.params.inid) - 1;
const next_id = Number(props.props.match.params.inid) + 1;
useEffect(() => {
getLessonTitles(id);
getLessonData(props.props.match.params.inid);
}, [props.props.match.params]);
<div>
<button type="button">
<Link to={"/lesson/" + id + "/" + prev_id}>PREV</Link>
</button>
<button type="button">
<Link to={"/lesson/" + id + "/" + next_id}>CONTINUE</Link>
</button>
</div>
I'm not sure what to do.
It all depends on how you set-up your Routes and data-flow. In any case, the component that contains your buttons needs to know just how many items there are in the list. That's really the best way to configure the pagination logic.
Consider an example like this with working sandbox: https://codesandbox.io/s/react-router-dom-using-render-prop-oznvu
App.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route } from "react-router-dom";
import Dashboard from "./Dashboard";
import Lesson from "./Lesson";
class App extends React.Component {
state = {
lessonData: []
};
componentWillMount() {
//fetch data and set-state here. Will use static list as an example
const data = [
{
id: 1,
description: "Math"
},
{
id: 2,
description: "Science"
},
{
id: 3,
description: "Art"
},
{
id: 4,
description: "English"
}
];
this.setState({
lessonData: data
});
}
render() {
return (
<BrowserRouter>
<Route
path="/"
exact
render={props => <Dashboard data={this.state.lessonData} />}
/>
<Route
path="/lesson/:id"
render={props => {
const id = props.match.params.id;
const data = this.state.lessonData.find(lesson => lesson.id == id);
return (
<Lesson
{...props}
selected={data}
lengthOfDataSet={this.state.lessonData.length}
/>
);
}}
/>
</BrowserRouter>
);
}
}
When the user navigates to the Lesson path, we will pass in a prop to the rendered Lesson component, the prop contains the number of lesson items retrieved from our data-set (using a static list for example). Additionally, we will use props.match.params.id to find the corresponding item in our data-set, and pass that item as a prop as well.
Lesson.js
import React from "react";
import { Link } from "react-router-dom";
const Lesson = props => {
const {
selected,
lengthOfDataSet,
match: {
params: { id }
}
} = props;
const createButtons = () => {
const prevLinkId = id == 1 ? lengthOfDataSet : parseInt(id) - 1;
const nextLinkId = id < lengthOfDataSet ? parseInt(id) + 1 : 1;
return (
<div>
<button type="button>
<Link to={`/lesson/${prevLinkId}`}>Prev</Link>
</button>
<button type="button">
<Link to={`/lesson/${nextLinkId}`}>Next</Link>
</button>
</div>
);
};
if (!selected) {
return <div>Loading...</div>;
} else {
return (
<div>
<Link to="/">Back to home</Link>
<h4>Id: {selected.id}</h4>
<h4>Description: {selected.description}</h4>
<div>{createButtons()}</div>
</div>
);
}
};
In the Lesson component, we can use the props passed to configure our Prev button and Next button logic.
For the Prev button, if the current id is 1, then clicking it would redirect you to a path using the last item in the dataset, otherwise go to previous item.
For the Next button, if the current id is less than the prop (lengthOfDataSet), then clicking it will go to the next item, otherwise go back to the beginning of the set.

Resources