react prop is undefined in child component - reactjs

Main.js
import axios from 'axios'
import React, { Component, useEffect, useState } from 'react'
import { BrowserRouter as Router, Switch, Route} from "react-router-dom";
import Books from './Books'
import Navbar from './Navbar'
import AddBook from './AddBook';
import BookDetail from './BookDetail';
const Main = () => {
let [selectedBook, setSelectedBook] = useState() // Storing the data from Books.js
let [books, setBooks] = useState([])
useEffect(() => {
axios.get('http://127.0.0.1:8000/api/get-books')
.then(res => {
setBooks(books = res.data)
})
})
return (
<Router>
<div>
<Navbar title={selectedBook}/> // is defined here <----
<div className='container'>
<Switch>
<Route exact path='/' >
<Books books={books} onBookSelect={(book) => {setSelectedBook(book)} }/> // Receiving the data from here
</Route>
<Route path='/add-book' >
<AddBook />
</Route>
<Route path={`/details`} >
<BookDetail bookId={selectedBook} /> // is undefined when passed here <----
</Route>
</Switch>
</div>
</div>
</Router>
)
}
export default Main
Books.js
import React, { Component, useState } from 'react'
import BookDetail from './BookDetail'
const Books = ({books, onBookSelect}) => {
const sendBookId = e => {
let bookId = e.target.value
onBookSelect('i am not undefined') // sending the data to Main.js
setTimeout(function(){ window.location.href='http://localhost:3000/details' }, 10)
}
return (
<div>
<div className='books'>
{books.map(book => (
<div className='book'>
<h2>{book.name}</h2>
<p>Author: <a href='#'>{book.author}</a></p>
<button className="btn" value={book.id} onClick={sendBookId}>View Details</button> // Getting the id from here
</div>
))}
</div>
</div>
)
}
export default Books
BookDetail.js
import React, { useEffect } from 'react'
import axios from 'axios'
import { useState } from 'react'
const BookDetail = ({bookId}) => {
return (
<div>
<p>book: {bookId}</p> // bookId is undefined
</div>
)
}
export default BookDetail
Sample from localhost:8000/api/get-books
{
"id": 5,
"name": "Book1",
"author": "Author1",
"content": "a normal book",
"isbn": "235456",
"passcode": "123",
"created_at": "2021-07-12T16:29:47.114356Z",
"pages": "3"
}
Basically, the data is sent from Book.js to the parent component which is Main.js and which is stored in selectedBook, and the data is defined and displays in the title, but when I add it as a prop in and try to access it from there it becomes undefined, what am I doing wrong?

Issue
Credit to Yoshi for calling it out first, but there is an issue with the way you navigate from your home page to the book details page. Mutating the window.location.href object will actually reload the page, and your app. Since React state lives in memory it is lost upon page reloading.
Solution
Use the history object provided by the Router context to PUSH to the new route. Since you are already using a function component you can import the useHistory React hook from react-router-dom and issue the imperative navigation.
import { useHistory } from 'react-router-dom';
...
const Books = ({ books, onBookSelect }) => {
const history = useHistory(); // <-- access the history object from hook
const sendBookId = (e) => {
const bookId = e.target.value;
onBookSelect(bookId);
history.push("/details"); // <-- issue PUSH to route
};
return (
<div>
<div className="books">
Books
{books.map((book) => (
<div className="book">
<h2>{book.name}</h2>
<p>
Author: {book.author}
</p>
<button className="btn" value={book.id} onClick={sendBookId}>
View Details
</button>
</div>
))}
</div>
</div>
);
};

Related

How to pass data from component to another component in react hooks with onClick

On click on the blogItem, how can I pass data from blogItem.js component to blogDetails.js component ? However the blogItem component is imported in Home.js component. May I know where do I add the click action ? Could someone please share an example ?
CodeSandboX link
https://codesandbox.io/s/gallant-water-5bmueq?file=/src/blogItem.js:0-1876
// App.js
import { BrowserRouter, Route, Routes, Switch } from "react-router-dom";
import Home from "./src/Home";
import BlogItem from "./src/blogItem";
function App() {
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/blogItem" element={<BlogItem />}></Route>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
//Home.js
import "./styles.css";
import React, { useState, useEffect, useCallback } from "react";
import BlogItem from "./blogItem";
const Home = (props) => {
const [blogItem, setBlogItem] = useState(props);
useEffect(() => {
setBlogItem(props);
}, [props]);
return (
<div id="App">
<BlogItem />
</div>
);
};
export default Home;
// BlogItem.js
import "./styles.css";
import React, { useState, useEffect, useCallback } from "react";
const blogData = [
{
id: 1,
date: "25 Jan 2023",
photo: "https://picsum.photos/80",
heading: "Cypress setup blog",
blogDetails:
"Best heading added here. The most relevant data added here. Greatest of all time. Wont be a good idea play here always."
},
{
id: 2,
date: "22 Jan 2022",
photo: "https://picsum.photos/80",
heading: "React state details",
blogDetails:
"Best heading added here. The most relevant data added here. Greatest of all time. Wont be a good idea play here always."
}
];
const BlogItem = ({ id, date, photo, heading, blogDetails }) => {
const [searchResults, setSearchResults] = useState(blogData);
const [state, setState] = useState({ id, date, photo, heading, blogDetails });
const Columns = () => (
<div className="blogItems">
<div className="row">
<div className="blogArea">
{searchResults.map(({ id, date, photo, heading, blogDetails }) => (
<a key={id}>
<div className="blogImageSection">
<img alt="id" src={photo} />
<div key={id} className="dataArea">
<span className="dataDate">{date}</span>
<span className="tags">cypress</span>
<h3>{heading}</h3>
<p>
Best heading added here. The most relevant data added here.
Greatest of all time. Wont be a good idea play here always.
</p>
<a href="_blank" className="readmoreLink">
Read more →
</a>
</div>
</div>
</a>
))}
</div>
</div>
</div>
);
return <Columns {...state} />;
};
export default BlogItem;
// BlogDetails.js
import React from "react";
const BlogDetails = () => {
return (
<div id="App">
<div className="blogDetailsSection">
<div className="row">
<div className="individual-blogs">
<h2>ddd</h2>
<p>Blog details</p>
<pre>
<code>const data = []; data.push("soccer");</code>
</pre>
</div>
</div>
</div>
</div>
);
};
export default BlogDetails;
First of all make the browserRouter in the index.js file and remove it from the app.js file and create a Route for the BlogDetails component :
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/blogItem" element={<BlogItem />}></Route>
<Route path="/blogDetails" element={<BlogDetails />}></Route>
</Routes>
</BrowserRouter>
Then you have to install 'react-router-dom' package.
npm i react-router-dom
In BlogItem.js file import useNavigate from it :
import { useNavigate } from "react-router-dom";
const navigate = useNavigate(); // from inside the function component
Now you add onClick to the element that you want to click on it to be redirect to /blogDetails for example here :
<div className="blogImageSection"
onClick={() =>
navigate('blogDetails', { state: {id, date, photo, heading, blogDetails } }) // this is how to pass data with useNavigate
}> ... </div>
Finally in BlogDetails.js file :
import { useLocation } from "react-router-dom";
const location = useLocation(); // from inside the function component
And you can access the passed data like this :
const myRecievedData = location.state

I want to pass my Sidebar to some views in React

Im building this app for a shop, I have the Login already working but I want that when Login in app, it shows the Sidebar, and when choosing an option, the Sidebar may not dissapear only the content part should change, but it doesn't, heres what I have:
App.js
import React from 'react';
import Login from './components/Login'
import Dashboard from './components/Dashboard';
import Administrar from './components/Productos/Administrar';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'
function App() {
return (
<React.Fragment>
<Router>
<Switch>
<Route path="/" exact render={ props => (<Login {...props} />)}></Route>
<Route path="/dashboard" exact render={ props => (<Dashboard {...props} />)}></Route>
</Switch>
</Router>
</React.Fragment>
);
}
export default App;
Dashboard.jsx
import React, { useState, useEffect} from 'react';
import Sidebar from '../template/Sidebar';
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import axios from 'axios';
import {ApiUrl} from '../services/apirest'
import Administrar from './Productos/Administrar'
const SideAndNavbar = () => {
return (
<React.Fragment>
<Sidebar/>
<Router>
<Switch>
<Route path="/productos/administrar" exact component={ props => (<Administrar {...props} />)}></Route>
</Switch>
</Router>
</React.Fragment>
)
}
const Relog = () => {
return (
<div>
<h1>Relogeate pá</h1>
</div>
)
}
export default function Dashboard() {
const [user, setUser] = useState(null)
useEffect(() => {
obtenerUsuario()
}, [])
const obtenerUsuario = async () => {
let url = ApiUrl + "/usuario";
await axios.get(url)
.then(response => {
setUser(response.data.user)
}).catch(error => {
console.log(error)
})
}
return (
(user ? <SideAndNavbar/>: <Relog/>)
);
}
Login.jsx
import React, { Component } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css'
import toast, {Toaster} from 'react-hot-toast'
import logo from '../assets/img/img-01.png'
import axios from 'axios'
import {ApiUrl} from '../services/apirest'
class Login extends Component {
// eslint-disable-next-line
constructor(props){
super(props);
}
state = {
form:{
"email": "",
"password": ""
},
}
manejadorChange = async(e) =>{
await this.setState({
form: {
...this.state.form,
[e.target.name]: e.target.value
}
})
}
manejadorBoton = () => {
let url = ApiUrl + "/auth/logearse";
axios.post(url, this.state.form)
.then(response => {
if(response.data.status === "OK"){
localStorage.setItem("token", response.data.token);
/*
this.props.history.push({
pathname: '/dashboard',
state: response.data.user
})
*/
this.props.history.push('/dashboard');
}else{
toast.error(response.data.message);
}
}).catch(error => {
console.log(error);
toast.error("Error al conectarse con el servidor");
})
}
manejadorSubmit(e){
e.preventDefault();
}
render() {
return (
<React.Fragment>
<Toaster position="top-center" reverseOrder={false}/>
<div className="limiter">
<div className="container-login100">
<div className="wrap-login100">
<div className="login100-pic">
<img src={logo} alt="Imagen"/>
</div>
<form className="login100-form validate-form" onSubmit={this.manejadorSubmit}>
<span className="login100-form-title">
Ingreso de Usuario
</span>
<div className="wrap-input100 validate-input" data-validate = "Valid email is required: ex#abc.xyz">
<input className="input100" type="text" name="email" placeholder="Email" onChange={this.manejadorChange}/>
<span className="focus-input100"></span>
<span className="symbol-input100">
<i className="fa fa-envelope" aria-hidden="true"></i>
</span>
</div>
<div className="wrap-input100 validate-input" data-validate = "Password is required">
<input className="input100" type="password" name="password" placeholder="Password" onChange={this.manejadorChange}/>
<span className="focus-input100"></span>
<span className="symbol-input100">
<i className="fa fa-lock" aria-hidden="true"></i>
</span>
</div>
<div className="container-login100-form-btn">
<button className="login100-form-btn" onClick={this.manejadorBoton}>
Ingresar
</button>
</div>
<div className="text-center p-t-56">
</div>
</form>
</div>
</div>
</div>
</React.Fragment>
);
}
}
export default Login;
What can I do? Here is some pics:
enter image description here
Looks like you are trying to achieve some heavy Routing without the help of any state management, There are few anti patterns in your current setup. I have a few suggestions that will help you achieve what you are trying to implement.
Firstly Your application routing is setup in a ambiguous way. If you see the routing implementation looks like a nested Routing setup. but the URl you are used is /productos/administrar this route in terms of relativity is rendered from the / base URL.
Your Home(Entry point of the application APP.js) is setup with a parent Router, The router now reads your URL and Tries to render a component. but sees that in the parent Route there is no such URL.
Now if you see carefully your dashboard component is setup to render <SideNav>, to render the sidenav on the /productos/administrar route you should firstly go through the dashboard component.
in your current setup the dashboard component is missed and directly the Admin component is rendered at the root of the router.
I would want you to follow the Layout Pattern where you can stuff the sidenav and topnav (if you have any) along with a main section which render the childrens passed to the component, and on each route call this Layout Component with children props.
this way you ensure the Layout is visible on every route. But that's a long way . If you want a quick fix is to implement proper nested Routing using useRouterMatch() to pass the current route down the component tree. Now change the dashboard component to something like this
const SideAndNavbar = ({match}) => {
return (
<React.Fragment>
// Make sure you user the match props in Sidebar to append he correct URl for nesting to the Link tag , for example (<Link to={`${match.url}/productos/administrar`}>GO to Admin</Link>)
<Sidebar match={match}/>
<Router>
<Switch>
<Route path={`${match.path}/productos/administrar`} exact component={ props => (<Administrar {...props} />)}></Route>
</Switch>
</Router>
</React.Fragment>
)
}
const Relog = () => {
return (
<div>
<h1>Relogeate pá</h1>
</div>
)
}
export default function Dashboard() {
const [user, setUser] = useState(null)
let match = useRouteMatch();
useEffect(() => {
obtenerUsuario()
}, [])
const obtenerUsuario = async () => {
let url = ApiUrl + "/usuario";
await axios.get(url)
.then(response => {
setUser(response.data.user)
}).catch(error => {
console.log(error)
})
}
return (
(user ? <SideAndNavbar match={match} />: <Relog/>)
);
}
For more info on this topic see the official documentation of React Router

react send data from child component to parent

Main.js (Parent)
import axios from 'axios'
import React, { Component, useEffect, useState } from 'react'
import { BrowserRouter as Router, Switch, Route} from "react-router-dom";
import Books from './Books'
import Navbar from './Navbar'
import AddBook from './AddBook';
import BookDetail from './BookDetail';
const Main = () => {
let [books, setBooks] = useState([])
useEffect(() => {
axios.get('http://127.0.0.1:8000/api/get-books')
.then(res => {
setBooks(books = res.data)
})
})
return (
<Router>
<div>
<Navbar title='LunaBooks'/>
<div className='container'>
<Switch>
<Route exact path='/' >
<Books books={books} />
</Route>
<Route path='/add-book' >
<AddBook />
</Route>
<Route path='/details/' >
<BookDetail bookName={} /> // put the bookName from Books.js here <----
</Route>
</Switch>
</div>
</div>
</Router>
)
}
export default Main
Books.js (Child)
import React, { Component, useState } from 'react'
import BookDetail from './BookDetail'
const Books = ({books}) => {
let [bookName, setBookName] = useState('') // send the bookname to Main.js
const SendBookDetails = e => {
let bookName = e.target.value
setBookName(bookName = bookName)
}
return (
<div>
<div className='books'>
{books.map(book => (
<div className='book'>
<h2>{book.name}</h2>
<p>Author: <a href='#'>{book.author}</a></p>
<button className="btn" value={book.name} onClick={SendBookDetails}>View Details</button>
</div>
))}
</div>
</div>
)
}
export default Books
To put it simply, I want to send the bookName that is located in Books.js and put in the Main.js so that I can pass it in BookDetail.js, I know how to do in the reverse way but this is just confusing me for some reason... I'm pretty new to react so please bear with me!
Make a selectedBook state in the parent.
const [selectedBook, setSelectedBook] = useState()
Pass a callback function as a prop "onBookSelect" to the child.
<Books books={books} onBookSelect={(book) => { setSelectedBook(book) } />
Then in the child call the callback function with new value. in your SendBookDetails function.
const Books = ({books, onBookSelect}) => {
//...
const SendBookDetails = e => {
let bookName = e.target.value
setBookName(bookName)
onBookSelect(bookName)
}
Then pass that state to the props of your book details component.
<BookDetail bookName={selectedBook} />

Not able to access detail information from Api using React Router not rendering on the page

I am building a small React Routing application to get a better idea as to how it work. My App.js looks like this with the basic routing:
import React from 'react';
import './App.css';
import Nav from './Nav';
import About from './About';
import Shop from './Shop';
import CountryDetail from './CountryDetail'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
function App() {
return (
<Router>
<div className="App">
<Nav />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/shop" exact component={Shop} />
<Route path="/shop/:name" component={CountryDetail} />
</Switch>
</div>
</Router>
);
}
const Home = () => (
<div>
<h1>Home Page</h1>
</div>
);
Now the Shop component a list of countries from the api which is in the code below:
import React from 'react';
import './App.css';
import { useEffect } from 'react';
import { useState } from 'react';
import {Link} from 'react-router-dom';
function Shop() {
useEffect(() => {
fetchItems();
},[])
const [countries, setCountries] = useState([])
const fetchItems = async () => {
const data = await fetch('https://restcountries.eu/rest/v2/all');
const countries = await data.json();
console.log(countries);
setCountries(countries);
}
return (
<div>
{countries.map(country => (
<div>
<Link to={`shop/${country.name}`}>
<h1 key={country.alpha2Code}>
{country.name}
</h1>
</Link>
<p>Popluation {country.population}</p>
<p> Region {country.region}</p>
<p>Capital {country.capital}</p>
</div>
)
)}
</div>
);
}
export default Shop;
Now what I want to do is render more information about the country when I click on it. So I have created another component called CountryDetail:
import React from 'react';
import './App.css';
import { useEffect } from 'react';
import { useState } from 'react';
function CountryDetail( { match } ) {
useEffect(() => {
fetchItem();
console.log(match)
},[])
const [country, setCountry] = useState([])
const fetchItem = async ()=> {
const fetchCountry = await fetch(`https://restcountries.eu/rest/v2/name/${match.params.name}`);
const country = await fetchCountry.json();
setCountry(country);
console.log(country);
}
return (
<div>
<h1>Name {country.name}</h1>
<p>Native Name{country.nativeName}</p>
<p>Region {country.region}</p>
<p>Languages {country.languages}</p>
<h1>This Country</h1>
</div>
);
}
export default CountryDetail;
The problem I am having is that it is not rendering anything on the CountryDetail page. I am sure I have hit the api correctly but not sure if I am getting the data correctly. Any help would be appreciated.
Issue: The returned JSON is an array but your JSX assumes it is an object.
Solution: You should extract the 0th element from the JSON array. Surround in a try/catch in case of error, and correctly render the response.
Note: the languages is also an array, so that needs to be mapped
function CountryDetail({ match }) {
useEffect(() => {
fetchItem();
console.log(match);
}, []);
const [country, setCountry] = useState(null);
const fetchItem = async () => {
try {
const fetchCountry = await fetch(
`https://restcountries.eu/rest/v2/name/${match.params.name}`
);
const country = await fetchCountry.json();
setCountry(country[0]);
console.log(country[0]);
} catch {
// leave state alone or set some error state, etc...
}
};
return (
country && (
<div>
<h1>Name {country.name}</h1>
<p>Native Name{country.nativeName}</p>
<p>Region {country.region}</p>
<p>Languages {country.languages.map(({ name }) => name).join(", ")}</p>
<h1>This Country</h1>
</div>
)
);
}
As you said it yourself, the response is an array (with a single country object in it), but you are using it as if it would be an object.
So, instead of:
const country = await fetchCountry.json();
setCountry(country);
It should be:
const countries = await fetchCountry.json();
setCountry(countries[0]);

react-router-dom <Link> Not Updating Page

Description of problem:
Changing the id (numbers only) of this url via the link tag does not update the page (but does change the url in the adress bar). Hitting refresh afterward will show the updated page.
http://localhost:8080/video/id/7564
Right clicking to open the link in a new tab, or changing the link path to a completely different page works as expected.
My app.js file
import React from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import RenderHomepage from '../components/homePage/RenderHomepage'
import RenderChannelPage from '../components/channelPage/RenderChannelPage'
import RenderVideoPage from '../components/videoPage/RenderVideoPage'
import RenderSearchPage from '../components/searchPage/RenderSearchPage'
import PageNotFound from '../components/PageNotFound'
import history from '../history'
const App = () => {
return (
<div>
<Router history={history}>
<Switch>
<Route path="/" exact component={RenderHomepage} />
<Route path="/channel" component={RenderChannelPage} />
<Route path="/video/id" component={RenderVideoPage} />
<Route path="/search" component={RenderSearchPage} />
<Route path="/404" exact component={PageNotFound} />
<Route component={PageNotFound} />
</Switch>
</Router>
</div>
)
}
export default App
Link tag in UpNextVideos component:
import React from 'react'
import { Link } from 'react-router-dom'
...
<Link to={{pathname: vid.id}}>
<h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
</Link>
...
How the components in question are nested:
<RenderVideoPage>
<VideoPage>
<UpNextVideos>
RenderVideoPage component:
import React from 'react'
import VideoPage from './VideoPage'
import Header from '../Header'
import HeaderMobile from '../HeaderMobile'
import FooterMobile from '../FooterMobile'
import ActivityFeed from '../ActivityFeed'
const RenderVideoPage = () => {
return (
<div className="videoPage-body">
<HeaderMobile />
<Header />
<ActivityFeed page={'home'} />
<VideoPage />
<FooterMobile page={'video'} />
</div>
)
}
export default RenderVideoPage
VideoPage component:
import React, { useEffect, useState } from 'react'
import axios from 'axios'
import history from '../../history'
import handleMediaQueries from './containers/handleMediaQueries'
import setDislikes from './containers/setDislikes'
import NewSubscribers from './NewSubscribers'
import CommentSection from './CommentSection'
import UpNextVideos from './UpNextVideos'
import DescriptionBox from './DescriptionBox'
import VideoNotFound from './VideoNotFound'
import { fetchVideoFromID, fetchPictureFromID } from '../../containers/api'
import { thumbsUp, thumbsDown } from '../svgs'
import {
abbreviateNumber,
capitalizeFirstLetter,
randomDate } from '../../containers/helperFunctions'
const VideoPage = () => {
const [p, setPrefix] = useState("videoPage")
const [state, setState] = useState({
loading: true,
error: false
})
useEffect(() => {
if (state.loading) extractDataFromUrl()
else handleMediaQueries()
}, [state.loading])
const fetchVideo = async (id, picAuthorID) => {
let response = await fetchVideoFromID(id)
if (!response) setState(prevState => ({...prevState, error: true}))
else mapVideoResponseToHTML(response.data.hits, picAuthorID)
}
const mapVideoResponseToHTML = (response, picAuthorID) => {
let responseAsHtml = response.map(vid => {
return {
video:
<div className={`${p}-video-wrapper posRelative`} key={vid.id}>
<a className={`${p}-pixabay-src`} href={vid.pageURL}>?</a>
<video
poster="https://i.imgur.com/Us5ckqm.jpg"
className={`${p}-video clickable`}
src={vid.videos.large.url}
controls autoPlay>
</video>
<div className={`${p}-video-info-wrapper`}>
<div className={`${p}-video-title-box`}>
<h1 className={`${p}-video-title`}>{capitalizeFirstLetter(vid.tags)}</h1>
<span className={`${p}-video-views`}>{abbreviateNumber(Number(vid.downloads).toLocaleString())} views</span>
<span className={`${p}-video-date`}>{randomDate()}</span>
</div>
<div className={`${p}-video-options`}>
<div className="thumbs">
<div className={`${p}-video-options-thumbsUp`}>{thumbsUp(20)}
<span className={`${p}-video-options-thumbsUp-text`}>{abbreviateNumber(vid.likes)}</span>
</div>
<div className={`${p}-video-options-thumbsDown`}>{thumbsDown(20)}
<span className={`${p}-video-options-thumbsDown-text`}>{setDislikes(vid.likes)}</span>
</div>
<div className={`${p}-video-options-likebar`}></div>
</div>
<span className={`${p}-video-options-share`}>Share</span>
<span className={`${p}-video-options-save`}>Save</span>
<span className={`${p}-video-options-ellipses`}>...</span>
</div>
</div>
</div>,
authorFollowers: vid.views,
vidAuthorID: vid.id,
author: picAuthorID ? 'Loading' : vid.user,
authorAvatar: picAuthorID ? null : vid.userImageURL,
views: vid.downloads
}
})
responseAsHtml = responseAsHtml[0]
setState(prevState => ({...prevState, ...responseAsHtml, loading: false}))
if (picAuthorID) fetchAuthorAvatar(picAuthorID)
}
const extractDataFromUrl = () => {
const currentURL = window.location.href
const urlAsArray = currentURL.split('/')
const urlID = urlAsArray[5].split('-')
const videoID = urlID[0]
const picAuthorID = urlID[1]
// Author avatars are random except on the home page.
// if url isnt from homepage, then use videoID
// if url is from homepage, send that avatarID
if (urlID.includes('000')) {
fetchVideo(videoID)
} else {
setState(prevState => ({...prevState, picAuthorID: picAuthorID}))
fetchVideo(videoID, picAuthorID)
}
}
const fetchAuthorAvatar = async (id) => {
const response = await fetchPictureFromID(id)
const authorName = response.data.hits[0].user
const authorAvatar = response.data.hits[0].previewURL
setState(prevState => ({
...prevState,
authorAvatar: authorAvatar,
author: capitalizeFirstLetter(authorName)
}))
}
return (
<div>
{ state.error ? <VideoNotFound /> : null}
{ state.loading === true ? null
:
<div className={`${p}-page-wrapper`}>
<main className={`${p}-main`}>
{state.video}
<DescriptionBox props={state} />
<div className={`${p}-suggested-videos-mobile`}></div>
<div className={`${p}-new-subscribers-wrapper`}>
<h2 className={`${p}-new-subscribers-text`}>{`New Subscribers to ${state.author}`}</h2>
<NewSubscribers />
</div>
<div className={`${p}-comment-section`}>
<CommentSection views={state.views}/>
</div>
</main>
<aside className={`${p}-sidebar`}>
<UpNextVideos />
</aside>
</div>
}
</div>
)
}
export default VideoPage
UpNextVideos component:
import React, { useEffect, useState, useRef, useCallback } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import { videoQuery } from '../../words'
import { fetchVideos } from '../../containers/api'
import {
capitalizeFirstLetter,
uuid,
getRandom,
abbreviateNumber
} from '../../containers/helperFunctions'
const UpNextVideos = () => {
const [p, setPrefix] = useState("videoPage")
const [nextVideos, setNextVideos] = useState([])
useEffect(() => {
fetchUpNextVideos(15, getRandom(videoQuery))
}, [])
// INFINITE SCROLL
const observer = useRef()
const lastUpNextVideo = useCallback(lastVideoNode => {
// Re-hookup observer to last post, to include fetch data callback
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver(entries => {
const lastVideo = entries[0]
if (lastVideo.isIntersecting && window.innerWidth <= 1000) {
document.querySelector('.videoPage-show-more-button').classList.add('show')
}
else if (lastVideo.isIntersecting && window.innerWidth > 1000) {
document.querySelector('.videoPage-show-more-button').classList.remove('show')
fetchUpNextVideos(20, getRandom(videoQuery))
}
})
if (lastVideoNode) observer.current.observe(lastVideoNode)
})
const fetchUpNextVideos = async (amount, query) => {
let response = await fetchVideos(amount, ...Array(2), query)
response = response.data.hits
const responseAsHtml = response.map((vid, index) => {
return (
<div className={`${p}-sidebar-grid-video-wrapper`} key={uuid()} ref={response.length === index + 1 ? lastUpNextVideo : null}>
<div className={`${p}-sidebar-grid-video`}>
<a href={`/video/id/${vid.id}-000`}>
<video
className={`${p}-upnext-video`}
onMouseOver={event => event.target.play()}
onMouseOut={event => event.target.pause()}
src={`${vid.videos.tiny.url}#t=1`}
muted >
</video>
</a>
</div>
<a href={`/video/id/${vid.id}`}>
<h3 className={`${p}-sidebar-grid-video-title`}>{capitalizeFirstLetter(vid.tags)}</h3>
</a>
<a href={`/channel/000${vid.id}`}>
<p className={`${p}-sidebar-grid-video-author`}>{vid.user}</p>
</a>
<p className={`${p}-sidebar-grid-video-views-text`}>{abbreviateNumber(vid.downloads)} views</p>
</div>
)
})
setNextVideos(prevState => ([...prevState, ...responseAsHtml]))
}
return (
<div>
<div className={`${p}-sidebar-text-top`}>
<span className={`${p}-sidebar-text-upnext`}>Up next</span>
<span className={`${p}-sidebar-text-autoplay`}>Autoplay</span>
</div>
<div className={`${p}-sidebar-grid-wrapper`}>
{nextVideos}
</div>
<button
className={`${p}-show-more-button`}
onMouseDown={() => fetchUpNextVideos(15, getRandom(videoQuery))}>
Show More
</button>
</div>
)
}
export default UpNextVideos
What I've tried:
Wrapping the <Link> tag with <Router history={history} />
Wrapping the <Link> tag with <BrowserRouter>
Wrapping the export statement withRouter(UpNextVideos)
Using a plain string instead of an object, as described in react-router-docs
Ok, I believe this issue lies in your VideoPage component.
useEffect(() => {
if (state.loading) extractDataFromUrl()
else handleMediaQueries()
}, [state.loading]);
You only ever have state.loading true once, when the component mounts. This only processes your URL once, so when the URL changes this component isn't aware of it.
This is your route currently
<Route path="/video/id" component={RenderVideoPage} />
now assuming your URLs are shaped "/video/id/" then you can define your route to have a parameter
<Route path="/video/id/:videoId" component={RenderVideoPage} />
If you wrap this component with react-router-dom's withRouter HOC you can easily get the id path param and add it to an effect to recompute all the video data.
export default withRouter(VideoPage)
withRouter injects the location, match, and history props from the closest Route ancestor. Here's an example of getting the id param and triggering an effect when its value updates.
const VideoPage = ({ match }) => {
const { params } = match;
useEffect(() => { /* do something with new id */ }, [params.videoId]);
}

Resources