NextJS: what is the proper way to handle dynamic route pages? - javascript

I'm working on a project in NextJS for the first time, and I'm wondering what the right way to handle dynamic routing is.
I have a http://localhost:3000/trips route which displays a page with list of cards, each of which represent a "trip":
When I tap on one of these cards, I want to navigate to a dynamic page for that route such as http://localhost:3000/trips/0b68a50a-8377-4720-94b4-fabdabc12da1
This is my folder structure:
As you can see, I already have the dynamic routes set up and it is working.
TripCard is the card component. TripComponent is a grid of TripCards. trips/index.tsx contains the TripsComponent (and other UI).
Currently I'm handling the dynamic route in TripCard as:
import { Trip } from './Models'
import { useRouter } from 'next/router'
const TripCard = ({ trip }: { trip: Trip }) => {
const router = useRouter()
return (
<div className="card bg-base-100 shadow-xl hover:bg-gray-100 active:bg-gray-300">
<div className="card-body" onClick={() => router.push('/trips/' + trip.id)}>
<h2 className="card-title">{trip.name}</h2>
<p>This is a trip!</p>
</div>
</div>
)
}
export default TripCard
And the dynamic page [tripId].tsx looks like:
import { NextPage } from 'next'
import { useRouter } from 'next/router'
const TripPage: NextPage = () => {
const router = useRouter()
const tripId = router.query.tripId
return (
<div>
<h1>This is {tripId}</h1>
</div>
)
}
export default TripPage
And TripsComponent.tsx:
import { Trip } from './Models'
import TripCard from './TripCard'
const TripsComponent = ({ trips }: { trips: Trip[] }) => {
return (
<div>
<div className="grid grid-cols-4 gap-4">
{trips.map((trip: Trip) => (
<div>
<TripCard trip={trip}></TripCard>
</div>
))}
</div>
</div>
)
}
export default TripsComponent
And trips/index.tsx:
import axios from 'axios'
import { GetStaticProps, InferGetStaticPropsType, NextPage } from 'next'
import { Trip } from '../../components/Models'
import TripsComponent from '../../components/TripsComponent'
const TripsPage: NextPage = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<div className="m-9">
<h1 className="mt-9 text-3xl font-bold text-slate-800">Your Trips</h1>
<div className="justify-end">
<button className="btn btn-primary">Add Trip</button>
</div>
<div className="divider"></div>
<TripsComponent trips={props.data} />
</div>
)
}
export const getStaticProps: GetStaticProps = async () => {
// fetches data and passes to props
}
export default TripsPage
I guess my question is, what is the proper way to do routing like this where I have cards, and each card will go to a dynamic URL with an associated page? In TripCard I have a hardcoded router.push:
<div className="card-body" onClick={() => router.push('/trips/' + trip.id)}>
But that doesn't seem like the right way to handle this. For example, what if I want to use TripCard in another view and go to another route?
What's the best way to structure code that performs this function in NextJS?

Maybe you want to pass the url as props and use NextJs <Link/>:
import { Trip } from './Models'
import { useRouter } from 'next/router'
import Link from 'next/link'
const TripCard = ({ trip, url }: { trip: Trip, url: string }) => {
const router = useRouter()
return (
<div className="card bg-base-100 shadow-xl hover:bg-gray-100 active:bg-gray-300">
<Link href={url} passHref>
<div className="card-body">
<h2 className="card-title">{trip.name}</h2>
<p>This is a trip!</p>
</div>
</Link>
</div>
)
}
export default TripCard
And then use the component like this:
<TripCard trip={trip} href={'/trips/' + trip.id}></TripCard>
Hope this help.

For dynamic routes, you will have to use getStaticPaths and getStaticProps to fetch the paths before and generate all the paths. When you export getStaticPaths from the page Nextjs will pre-render all the paths specified by getStaticPaths.
getStaicPaths has to return two things paths and fallback. paths property will have to be an array of params. And fallback means should Nextjs look for pages and pre-render if not pre-rendered before. Suppose after publishing the site you created a new card. So, If the fallback is true NextJs will look for the card in the database and pre-render then store it in the file system. If fallback is false it will show 404 not found.
Also Instead of using router.push use Link component.
https://nextjs.org/docs/api-reference/data-fetching/get-static-paths

The routing you have implemented is correct only. Maybe you can try like this
For trips, you can navigate to /trips and for each trip /trip/<trip id>
If you want to make the trip URL dynamic then you can pass the URL itself to TripCard component.
url will be <route name>/<trip id>
<TripCard trip={trip} url={url}></TripCard>

Related

Why don't my images and information render on my React localhost?

Recently I started a React course where the chapter goal is to create a monster sort of website. Below I will leave the code of the relevant JS and JSX files. My SearchBox input does appear on my screen, however, the monster images with their respective h2 and p are not appearing only in my react localhost. I have tried going through my code to understand why my code is not working and I haven't been able to find a solution. Here I will leave the link to the API where I am obtaining the images from, you just have to change the number before the ? to access the other images. I am aware that classes are a bit outdated due to hooks but the course is focusing on them initially so that we can understand their behavior, so please do not update the code, just help me with the functionality.
https://robohash.org/1?set=set2&size=180x180
App.js File
import React, {Component} from 'react';
import './App.css';
import { CardList } from './components/card-list/card-list';
import {SearchBox} from './components/search-box/search-box';
class App extends Component {
constructor() {
super();
this.state = {
monsters: [],
searchField: ''
};
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(users => this.setState({monsters: users}));
}
render() {
const {monsters, searchField} = this.state;
const filteredMonsters = monsters.filter(monster => monster.name.toLowerCase().includes(searchField.toLowerCase())) //This checks if one of the monsters name includes the text inputted into the search field, and filters out those that do not. We lower case it to avoid capital and lower case issues.
return (
<div className='App'>
<SearchBox
placeholder='Search Monsters'
handleChange={e => this.setState({searchField: e.target.value})}
/>
<CardList monsters={filteredMonsters}/>
</div> //By using filteredMonsters here, initially all will appear but as we place some input, only those that include the searchField will appear
)
}
};
export default App;
card.jsx file
import React from 'react';
export const Card = props => {
return (
<div>
<img src={'https://robohash.org/'+ props.monster.id + '?set=set2&size=180x180'} alt='monster'/>
<h2> {props.monster.name} </h2>
<p> {props.monster.email} </p>
</div>
)
}
card-list.jsx file
import React from 'react';
import { Card} from '../card/card';
import './card-list.css';
export const CardList = props => {
return (
<div className='card-list'>
{props.monsters.map((monster) => {
<Card key={monster.id}/>
})}
</div>
)
};
In CardList, you never even passed a monster object to your Card component. Only a monster id for a key. Basically, you need something like this:
<div className='card-list'>
{props.monsters.map((monster) => {
<Card key={monster.id} monster={monster} />
})}
</div>
Minor suggestion: Use proper destructuring in Card component:
export const Card = ({ monster: { id, name, email } }) => {
return (
<div>
<img src={'https://robohash.org/'+ id + '?set=set2&size=180x180'} alt='monster'/>
<h2> {name} </h2>
<p> {email} </p>
</div>
)
}
Working Sandbox: https://codesandbox.io/s/fervent-cerf-qcubt?file=/src/App.js

Reactjs, Passing data(id) from one component to another in order to download a song

Hi I am new to Reactjs and I'm practicing on a project of a music website. But there is an issue which I'm stuck with.
Well there is a song page where I should show some songCards.
import React from 'react'
import SongCard from '../components/SongCard';
import { songsList } from '../components/songsList';
//this is first object of the songsList file which i hard coded instead of using api:
export const songsList =[
{
id : 1,
url : './components/Player/Songs/Homayoun Shajarian - Souvashoun.mp3',
image : require('../assets/Souvashoun.jfif').default,
name : 'x',
singer : 'y',
style :'z',
lyrics :'v',
source : './Palyer/Songs/Homayoun Shajarian - Souvashoun.mp3'
}];
export default function Songs() {
return(
<div>
<SongCard className='col-md-6' id={songsList[0].id} image={songsList[0].image} name={songsList[0].name} singer={songsList[0].singer}/><br/>
<SongCard className='col-md-6' id={songsList[1].id} image={songsList[1].image} name={songsList[1].name} singer={songsList[1].singer}/><br/>
<SongCard className='col-md-6' id={songsList[2].id} image={songsList[2].image} name={songsList[2].name} singer={songsList[2].singer}/><br/>
</div>
);
}
and in the SongCard which I'll attach the code bellow, there is a button intended to route us to a downloadMusic page in order to download the specific song that it's button was clicked:
import React from 'react'
import {Card, Button} from 'react-bootstrap';
import styled from 'styled-components';
import {BrowserRouter as Router} from 'react-router-dom';
function SongCard(props){
const id = this.props.id;
this.props.history.push({
pathname: '/downloadMusic',
search: '?query=downloadMusic',
state: { data: id }
})
return(
<Styles>
<Router>
<Card style={{ width: '18rem'}} className="cardbg shadow" text="white">
<Card.Img variant="top" src={props.image} className="img-fluid" />
<Card.Body className="text-center">
<Card.Title>آهنگ {props.name}</Card.Title>
<Card.Text>خواننده {props.singer}
</Card.Text>
<Button href="/DownloadMusic/:id" className="btn btn-outline-dark">go to download page</Button>
</Card.Body>
</Card>
</Router>
</Styles>
);
}
export default SongCard;
And finally this is the last page which is downloadMusic. Here also there should be a card containing information about the specific song and the final download button.
import React from 'react';
import { songsList } from './songsList';
import DownloadCard from './DownloadCard';
import AudioPlayer from './Player/AudioPlayer';
function DownloadMusic() {
const id = this.props.location.state.data;
const song = songsList.find(item => item.id === id);
return(
<div>
<DownloadCard className='col-md-6' id= {song.id} image={song.image} name={song.name} singer={song.singer}/><br/>
<AudioPlayer/>
</div>
);
}
export default DownloadMusic;
What I want is to send the id and url of that specific song that was clicked in the Song page, to downloadMusic page and show it's information from songsList in a card. I've tried so many ways like using state, useParams(), ... but none of them worked for me. Also I don't want to send the whole props because I know it's not the best practice. So what am I doing wrong?
Hope I've shared enough information. any help is appreciated.
what you have to do is map the array of songs, like this
import React from 'react'
import React from 'react'
import SongCard from '../components/SongCard';
import { songsList } from '../components/songsList';
//this is first object of the songsList file which i hard coded instead of using api:
const songsList =[
{
id : 1,
url : './components/Player/Songs/Homayoun Shajarian - Souvashoun.mp3',
image : require('../assets/Souvashoun.jfif').default,
name : 'x',
singer : 'y',
style :'z',
lyrics :'v',
source : './Palyer/Songs/Homayoun Shajarian - Souvashoun.mp3'
}];
export default function Songs() {
return(
<div>
{
songsList.map((song, idx) => {
return <SongCard key={idx} className='col-md-6' id={song.id} image={song.image} name={song.name} singer={song.singer}/>
})
}
</div>
);
}
Alright I finally found a way to do this, after a long effort! I'm gonna share it here if someone needed.
So in the SongCard.js I used useHistory() and a navigate function to save the id and navigate to downloadMusic page also in the onClick attribute of button used navigate function:
import React from 'react'
import {Card, Button} from 'react-bootstrap';
import styled from 'styled-components';
import {BrowserRouter as Router, useHistory} from 'react-router-dom';
function SongCard(props){
//navigating and saving id
const history = useHistory()
const navigate = (id) => {
history.push(`/downloadMusic/${id}`)
}
return(
<Styles>
<Router>
<Card style={{ width: '18rem'}} className="cardbg shadow" text="white">
<Card.Img variant="top" src={props.image} className="img-fluid" />
<Card.Body className="text-center">
<Card.Title>آهنگ {props.name}</Card.Title>
<Card.Text>خواننده {props.singer}
</Card.Text>
//make sure you add onClick attribute!
<Button onClick={() => navigate(props.id)} className="btn btn-outline-dark">download</Button>
</Card.Body>
</Card>
</Router>
</Styles>
);
}
export default SongCard;
Then in the DownloadMusic.js by using useRouteMatch() I accessed id param, this id was string so I converted it to number in the next step. At the end by using .find() method I found the specific song that was clicked for!
import React from 'react';
import { songsList } from './songsList';
import DownloadCard from './DownloadCard';
import AudioPlayer from './Player/AudioPlayer';
import { useRouteMatch } from "react-router-dom";
function DownloadMusic() {
//getting id param
const {
params: { id },
} = useRouteMatch('/downloadMusic/:id');
//converting the string id to number:
const num = Number(id);
//finding the song with song.id= num
const song = songsList.find(item => item.id === num);
return(
<div>
<DownloadCard className='col-md-6'
id= {song.id}
url= {song.url}
image={song.image}
name={song.name}
singer={song.singer}
/><br/>
<AudioPlayer id = {song.id}/>
</div>
);
}
export default DownloadMusic;
Just be careful about type of id, it must be number. this little one took me a long time.
:) Good luck!
You can create a function like this in the song card page
const onSongCardClick = () => {
history.push({
pathname: '/downloadMusic',
state: {id: this.props.id, url: this.props.url}
})
}
...
...
<Button onclick="onSongCardClick()" className="btn btn-outline-dark">go to download page</Button>
And get those values in Download music page by
const { id, url } = this.props.location.state
Also pass url as props to song card from songs page

How to display a particular component with different API data on different pages

I am new to react and there is this challenge that i am having,
I have slider created a component
import React from 'react'
import NextEvent from '../nextEvent/NextEvent'
import './slider.css';
function Slider(props) {
const {id, image, sub_title, title} = props;
return (
<main id='slider'>
<div className="slide" key= {id}>
<div className="slide-image">
<img src={image} alt="slider-background"/>
</div>
<h1>{title} </h1>
<h5>...{sub_title}</h5>
</div>
<div className="event-countdown">
<NextEvent/>
</div>
</main>
)
}
export default Slider
I need to have this banner component on almost all my pages, and on each of the pages, it comes with a
different information (image, title, subtitle)
the backend guy sent the api and i consumed, but the problem is that, if i consume the api on the
component directly, all the pages will have the same info on the banner component which is not what i want,
also consuming the API on the homepage seemed like it was not the right thing to do, so i created another component which
collects the Api and i then added that new component to my homepage.
now my question goes:
did i do the correct thing ?
if correct, does it mean i have to create new corresponding components that will receive the APi for
each page i want to display the banner just like i did for the homepage?
will i have to as the backend guy to create different apis for each of the pages in which the
component is to be displayed
if no please help me with an efficient way which i can inject data coming from the backend into a
component which will be displayed on different pages with different data
this is the new component i created for the APi consumption
import React, {useState, useEffect } from 'react'
import NextEvent from '../../components/nextEvent/NextEvent'
import axios from "axios";
import '../../components/slider/slider.css';
const sliderUrl = "*************************************"
function HomeSlider(props) {
const [sliderData, setSliderData] = useState([]);
const { image, sub_title, title} = props;
const getSliderContents = async () => {
const response = await axios.get(sliderUrl);
const content = response.data;
setSliderData(content);
}
useEffect(() => {
getSliderContents();
}, [])
// console.log("slider", sliderData)
if(sliderData) {
return (
<main id='slider'>
{sliderData.map((item) => {
return (
<div className="slide" key= {item.id}>
<div className="slide-image">
<img src={item.image} alt="slider-background"/>
</div>
<h1>{item.title} </h1>
<h5>...{item.sub_title}</h5>
</div>
)
})}
<div className="event-countdown">
<NextEvent/>
</div>
</main>
)
}
}
export default HomeSlider
this is the Homepage i displayed it
function HomePage() {
return (
<div>
<NavBar/>
<SecondaryMenu/>
<HomeSlider />
<FeaturedBox />
Please any help is appreciated, i have search all over but no one explains how to display
component with different data on different pages
So i just wanted to get back on this, i figured i have to setup a service point where i call the api and then consume the endpoints on each page as desired

Importing FontAwesome icons by string array in React.js

I'm having an array of sidebar elements in my React.js project where each element is represented as object which among others has its own FontAwesome icon defined as string, like e.g. fa-phone. Now there's a problem with FontAwesome's integration into React.js; each icon has to be separately imported and added to the library, according to their manual.
import * as React from 'react';
import { library } from '#fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
interface SidebarElement {
fa: string,
href: string,
title: string
}
interface SidebarElements {
elements: SidebarElement[]
}
export default class Sidebar extends React.Component<SidebarElements, {}> {
render() {
const elements = this.props.elements.map((element, key) => {
// tweak icon name so it matches component name...?
...
// the two lines below obviously won't work
import { element.fa } from '#fortawesome/free-solid-svg-icons'
library.add(element.fa);
return (
<li key={key} className="nav-item">
<a className="nav-link" href={element.href}>
<FontAwesomeIcon icon={element.fa} />
<span>{element.title}</span>
</a>
</li>
);
})
return (
<ul className="sidebar navbar-nav">{elements}</ul>
);
}
}
But the solution above obviously won't work, since imports have to happen at top-level and won't take the component name from a variable. Are there any alternative ways to import icons without exactly knowing them from the beginning? Or am I forced to import all icons at the same point I'm defining my sidebar elements?
I went with this same issue on a personal project I'm building. The first problem I found was related to how dynamically rendering the icon from a query?
Main app container:
import React from "react"
import Header from "../components/header"
import Navigation from "../components/navigation"
import Footer from "../components/footer"
import containerStyles from "./styles.module.less"
import { library } from "#fortawesome/fontawesome-svg-core"
import { fab } from "#fortawesome/free-brands-svg-icons"
library.add(fab)
const IndexPage = ({ data }) => (
<div className={containerStyles.home}>
<div className={containerStyles.menu}>
<Header />
<Navigation />
</div>
<Footer />
</div>
)
export default IndexPage
Also, my icons are part of the free-brand version so imported them to the library.
So the first thing I did was to import the library and create a pair of null variables on my child component, one for the prefix and the other one for the icon itself:
In my project, I'm consuming the data from an API endpoint, the query I built to get the information is the following:
Theoretically, all was set just for mapping the array and render each item as we normally do:
<FontAwesomeIcon
icon={[
(faprefix = document.node.prefix),
(faicon = document.node.icon),
]}
size="lg"
/>
But the child component was rendering nothing. Why was this? Simply because of both document.node.prefix and document.node.icon are returning strings so when the component mapped the data from the array, it ended trying to render something like this:
<svg data-prefix="'fab'" data-icon="'github'" >
Please note the single quotation mark wrapping the string
My solution to this was to use a replace() method with a regex to remove the wrapping quotations marks:
<FontAwesomeIcon
icon={[
(faprefix = document.node.prefix.replace(/'/g, "")),
(faicon = document.node.icon.replace(/'/g, "")),
]}
size="lg"
/>
Child footer component
import React from "react"
import { StaticQuery, graphql } from "gatsby"
import containerStyles from "../pages/styles.module.less"
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome"
let faicon = null
let faprefix = null
const Navigation = ({ data }) => (
<StaticQuery
query={graphql`
query FooterTemplate {
allStrapiLink {
edges {
node {
id
icon
url
prefix
}
}
}
}
`}
render={data => (
<>
<footer>
<p>Freddy Polanía {new Date().getFullYear()}</p>
<div className={containerStyles.links}>
{data.allStrapiLink.edges.map(document => (
<div key={document.node.id}>
<a
href={document.node.url}
rel="noopener noreferrer"
target="_blank"
>
<FontAwesomeIcon
icon={[
(faprefix = document.node.prefix.replace(/'/g, "")),
(faicon = document.node.icon.replace(/'/g, "")),
]}
size="lg"
/>
</a>
</div>
))}
</div>
</footer>
</>
)}
/>
)
export default Navigation
Now my icons are rendering from the endpoint's data. I hope this could help to solve your issue.

What is the best way to go about conditional rendering in the case of very similar components?

I render different landing pages based on whether the user is a professor, student, or not logged in. The landing pages are very similar; the only difference is the buttons displayed. I know I can go around this using inline conditions or simple if-else statements. However, I was wondering what the best practices are to implement conditional rendering in this case. I know higher order components (HOCs) can help but I was not sure if they are overkill in this particular case.
To be on the same page, here are the different Landing components that I currently render using if-else statements.
Landing.js (unlogged users):
import React from 'react';
import { withRouter } from 'react-router-dom';
import { compose } from 'recompose';
import { withEither } from '../../helpers/withEither';
import LandingStudent from './LandingStudent';
import LandingProfessor from './LandingProfessor';
import './Landing.css';
const Landing = ({ history }) => {
return(
<div className="header">
<div className="text-box">
<h1 className="header-primary">
<span className="header-primary-main">
QME
</span>
<span className="header-primary-sub">
the best way to ask questions
</span>
</h1>
</div>
</div>
);
};
export default Landing;
LandingProfessor.js
import React from 'react';
import { withRouter } from 'react-router-dom';
import RaisedButton from 'material-ui/RaisedButton';
import './Landing.css';
const LandingProfessor = ({ history }) => {
return(
<div className="header">
<div className="text-box">
<h1 className="header-primary">
<span className="header-primary-main">
QME
</span>
<span className="header-primary-sub">
the best way to ask questions
</span>
</h1>
<RaisedButton
className="btn-animated btn-landing"
label="Create Class"
onClick={() => history.push('/courses/new')}
/>
<RaisedButton
className="btn-animated btn-landing"
label="Dashboard"
onClick={() => history.push('/courses')}
/>
</div>
</div>
);
};
export default withRouter(LandingProfessor);
LandingStudent.js
import React from 'react';
import { withRouter } from 'react-router-dom';
import RaisedButton from 'material-ui/RaisedButton';
import './Landing.css';
const Landing = ({ history }) => {
return(
<div className="header">
<div className="text-box">
<h1 className="header-primary">
<span className="header-primary-main">
QME
</span>
<span className="header-primary-sub">
the best way to ask questions
</span>
</h1>
<RaisedButton
className="btn-animated btn"
label="Join Class"
onClick={() => history.push('/courses/join')}
/>
</div>
</div>
);
};
export default withRouter(Landing);
A 'trick' could be to append a className on the root div for landscape, namely 'student' 'professor' or 'logout' and use css display: none to hide unwanted items in each scenarios
Another approach would be to keep your 3 components but delegate all the rendering to a common 'LandingRenderer' component that would accept boolean properties like 'showJoinClassButton' 'showCreateButtonButton' etc.
Then your 3 components render would look like domething like this
LandingProfessor: (props) => (
<LandingRenderer
showJoinClassButton={false}
showCreateClassButton={true}
...
{...props} />
)
I would opt for composition over inheritance here, meaning create many reusable components and compose your top-level components with those reusable ones rather than make your top-level components inherit from one common component type. If you follow the latter approach, you might end up with a laundry list of props that I argue is questionably better than what you have right now.
To start, you could componentize your header:
Header.js
export default function Header(props) {
return (
<h1 className="header-primary">
{props.children}
</h1>
);
}
Header.Main = function HeaderMain(props) {
return (
<span className="header-primary-main">
{props.children}
</span>
);
};
Header.Sub = function HeaderSub(props) {
return (
<span className="header-primary-sub">
{props.children}
</span>
);
};
and use it in Landing.js:
import Header from './Header.js';
const Landing = ({ history }) => {
return(
<div className="header">
<div className="text-box">
<Header>
<Header.Main>QME</Header.Main>
<Header.Sub>the best way to ask questions</Header.Sub>
</Header>
</div>
</div>
);
}
I don't think hoc in this particular case is an overkill.
I think whenever you can use hoc for better readability and useability, use it.
Using some of recompose hocs (you should install recompose: npm install recompose)
export const renderByConditions = (...conditions) => compose(
...conditions.map(condition =>
branch(condition[0], (condition[1] && renderComponent(condition[1])) || renderNothing, condition[2] && renderComponent(condition[2]))
)
)
You should pass arrays with the signature:
[condition, left (if the condition is true), right (else)]
condition - (ownProps) => youCondition
left - Component | empty
right - Component | empty
when left is empty - if the condition is true, it will render null.
when right is empty - if the condition is not true, it will render the component we wrapped
Then you can use the hoc:
renderByConditions(
[props => props.landing, props => <Landing {...props}/>],
[props => props.proffesor, LandingProfessor],
[props => props.student, LandingStudent],
[Component => Component, DefaultComponent]
)
I would reccommend start using recompose hocs, they are great!

Categories

Resources