Nested React Router Component Won't Render on New Page - reactjs

I have a react app that consists of a main Component. , which comprises a element with navbar markup.
//app.js
import React, { Component } from 'react';
import {
Route,
NavLink,
BrowserRouter,
Switch,
} from "react-router-dom";
import Home from './components/home';
import Create from './components/create';
import './App.css';
const pathBase = 'http://127.0.0.1:8000';
const pathSearch = '/posts/';
class App extends Component {
constructor(props) {
super(props);
this.state = {
posts: [],
post: null,
error: null,
}
}
setPostsState = result => {
this.setState({ posts: result})
}
setPostState = post => {
this.setState({ post: post});
console.log(this.state.post)
}
retrievePost = (event, post) => {
event.preventDefault();
this.fetchPost(post);
}
deletePosts = id => {
fetch(`${pathBase}${pathSearch}${id }`, { method: 'DELETE'})
.then(res => res.status === 204 ? this.fetchPosts() : null)
}
editPosts = id => {
fetch(`${pathBase}${pathSearch}${id }`, { method: 'PUT'})
.then(res => res.status === 204 ? this.fetchPosts() : null)
}
fetchPost = (post) => {
fetch(`${pathBase}${pathSearch}${post.id}`)
.then(response => response.json())
.then(result => this.setPostState(result))
.catch(error => console.log(error));
}
fetchPosts = () => {
fetch(`${pathBase}${pathSearch}`)
.then(response => response.json())
.then(result => this.setPostsState(result))
.catch(error => this.setState({ error: error }));
}
componentDidMount() {
this.fetchPosts();
}
render() {
const { posts, error } = this.state;
if (error) {
return <div className="container">
<h3>Error: {error.message}</h3>
</div>;
}
return (
<BrowserRouter>
<div className="container">
<header>
<nav className="navbar navbar-expand-md navbar-light bg-light">
<a className="navbar-brand" href="">Django Rest API</a>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav mr-auto">
<li className="nav-item active">
<NavLink to="/posts">Posts</NavLink>
</li>
<li className="nav-item">
<NavLink to="/create">Create</NavLink>
</li>
</ul>
</div>
</nav>
</header>
<Switch>
<Route path="/create"
render={(props) =>
<Create {...props}
fetchPosts={this.fetchPosts}
/>
}
/>
<Route path="/posts"
render={(props) =>
<Home {...props}
posts={posts}
deletePost={this.deletePosts}
/>
}
/>
</Switch>
</div>
</BrowserRouter>
);
}
}
export default App;
There is also a (using React-Router v4) which path="/posts" that renders a Component . This component consists of a list which maps over blog posts obtained from the DB. Each post displays as a card with the post name serving as a link, that when the user clicks should bring up the component to show Post details.
// components/home.js
import React, { Component } from 'react';
import Post from './post';
import {
Route,
NavLink,
} from "react-router-dom";
class Home extends Component {
render() {
const ulStyle = { listStyleType: 'none' }
const { deletePost, posts } = this.props;
const match = this.props;
console.log(match)
return (
<ul style={ ulStyle }>
<h3>Posts</h3>
{posts.map(post =>
<li
key={posts.indexOf(post)}
value={post.id}
>
<div className="card">
<div className="card-body">
<NavLink to={`/posts/${post.id}`}>{post.title}
</NavLink>, Created: {post.timestamp}
<button type="button"
onClick={() => deletePost(post.id)}
className="float-right btn btn-outline-danger btn-sm"
>
Delete
</button>
</div>
</div>
</li>
)}
<Route exact path={`/posts/:postId`} render={(props) =>
(<Post {...props} posts={posts}/>)}/>
</ul>
);
}
}
export default Home;
I am having trouble getting the post component to render on a new page. Currently if the user clicks a link, the component renders at the bottom of the existing page (the component). I need the Post component to render on a entirely new page. How did I do this?
import React, { Component } from 'react';
class Post extends Component {
render() {
const { children, match, posts} = this.props;
console.log(match);
console.log(posts);
const p = (posts) => {
posts.find(post => post === match.postId)
}
return (
<div>
{match.params.postId}
<p>HELLO WORLD</p>
</div>
);
}
}
export default Post;

What you've seen is an expected behaviour for React Router nested routes. Content of the nested route will be rendered as part of it's parent component.
It's designed this way so that the number of re-renderings will be kept to minimum when navigation (the content of the parent component will not be re-rendered when the nested routes changed).
If you want the whole page content to be changed, you should not use nested route. Simply put all the routes you have under the Switch component. In other words, move your single post route to App component.

you should have a div to the router components. for example
<Router>
<div className = "app">
<nav />
<div className = "container">
//routes to be render here
<Route exact path = "/sample" component = {Sample} />
</div>
<footer />
</div>
</Router>
so that the component will be render on the middle of the page and remove the current if the route changes.

Related

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 router - click on card and redirect to a new page with content of that card (card details) using useParams and reacthooks

I have created a blog post. Posts are in the card[Event.js] and on click of the button. It should go to the new page and render its card details there. How can I do it using the react hooks and useParams.?
EventList.js ---> is where I'm fetching the data from api
Event.js ---> I'm rendering the fetched data in cards
EventDetails.js ---> It's the card details that should render on the screen when clicked on the post. Right now I have hard coded. the details.
Could someone please help me with how to do this?
//EventList.js
import React, { useState, useEffect } from "react";
import Event from "../Event/Event";
import axios from "axios";
import styles from "./EventList.module.css";
const EventList = () => {
const [posts, setPosts] = useState("");
let config = { Authorization: "..........................." };
const url = "............................................";
useEffect(() => {
AllPosts();
}, []);
const AllPosts = () => {
axios
.get(`${url}`, { headers: config })
.then((response) => {
const allPosts = response.data.articles;
console.log(response);
setPosts(allPosts);
})
.catch((error) => console.error(`Error: ${error}`));
};
return (
<div>
<Event className={styles.Posts} posts={posts} />
</div>
);
};
export default EventList;
//Event.js
import React from "react";
import styles from "./Event.module.css";
import { Link } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
const Event = (props) => {
const displayPosts = (props) => {
const { posts } = props;
if (posts.length > 0) {
return posts.map((post) => {
return (
<div>
<div>
<div className={styles.post}>
<img
src={post.urlToImage}
alt="covid"
width="100%"
className={styles.img}
/>
<div>
<h3 className={styles.title}>{post.title}</h3>
<div className={styles.price}> {post.author} </div>
<Link to={`/${post.title}`}>
<button className={styles.btns}> {post.author} </button>
</Link>
</div>
</div>
</div>
</div>
);
});
}
};
return <div className="Posts">{displayPosts(props)}</div>;
};
export default Event;
//EventDetails.js
import React, { useState, useEffect } from "react";
import Navbar from "../Navbar/Navbar";
import DetailsImage from "../../assets/Event-Ticketing.png";
import styles from "./EventDetails.module.css";
import "bootstrap/dist/css/bootstrap.min.css";
import { Link, useParams, useLocation } from "react-router-dom";
import axios from "axios";
// let config = { Authorization: "3055f8f90fa44bbe8bda05385a20690a" };
// const baseurl = "https://newsapi.org/v2/top-headlines?sources=bbc-news";
const EventDetails = (props) => {
const { state } = useLocation();
if (!state) return null;
// const [title, setTitle] = useState("");
// const { eventName } = useParams();
// useEffect(() => {
// axios
// .get(`${baseurl}`, { headers: config })
// .then((response) => setTitle(response.data));
// }, []);
// useEffect(() => {
// const neweventName = baseurl.find(
// (eventNames) => eventNames.eventName === parseInt(eventName)
// );
// setTitle(neweventName.title);
// }, []);
return (
<div>
<Navbar />
<div className={styles.eventBg}>
<div className="container">
<div>
<img
src={DetailsImage}
alt="ticket"
width="100%"
className={styles.heroEventImage}
/>
</div>
<div className={styles.bookingDetails}>
<div className={styles.nameBook}>
<div>
<div className={styles.eventNameHeader}>
<h1 className={styles.eventName}> {props.title}</h1>
</div>
<div className={styles.genre}>
<div className={styles.genreText}>{props.author}</div>
</div>
</div>
<div className={styles.bookingBtn}>
<div className={styles.booking}>
<Link to="/GeneralBooking">
<button
className={styles.bookBtn}
style={{ height: "60px", fontSize: "18px" }}
>
Book
</button>
</Link>
</div>
</div>
</div>
<div className={styles.venueTime}>
<div className={styles.dateTime}>
<div className={styles.dateTimeText}>{props.author}</div>
<div className={styles.price}>{props.author}</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default EventDetails;
//App.js
import "./App.css";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Home from "./components/Home/Home";
import EventDetails from "./components/EventDetails/EventDetails";
import GeneralBooking from "./components/GeneralBooking/GeneralBooking";
import AllotedSeated from "./components/AllotedSeated/AllotedSeated";
import Checkout from "./components/Checkout/Checkout";
function App() {
return (
<BrowserRouter>
<div className="App">
<Switch>
<Route path="/" exact>
<Home />
</Route>
<Route path="/:title" exact children={<EventDetails />}></Route>
<Route path="/GeneralBooking" exact>
<GeneralBooking />
</Route>
</Switch>
{/* <EventDetails /> */}
{/* <GeneralBooking /> */}
{/* <AllotedSeated /> */}
{/* <Checkout /> */}
</div>
</BrowserRouter>
);
}
export default App;
Since it doesn't appear as though you've stored the posts state sufficiently high enough in the ReactTree to be accessible by component on other routes I suggest using route state to send a specific post object to a receiving route.
Event - Update the Link to send also the post object.
<Link
to={{
pathname: `/${post.title}`,
state: { post },
}}
>
<button type="button" className={styles.btns}>{post.author}</button>
</Link>
EventDetails - Use the useLocation hook to access the route state.
import { useLocation } from "react-router-dom";
const EventDetails = (props) => {
const { state } = useLocation();
if (!state.post) return null;
return (
// ...render all the post fields available from state.post
// i.e. state.post.title
);
};

React Router - Path with :id is not working correctly for component wrapped by HOC

Hi I have been developing this application using react and react-router-dom The Page component is wrapped by a HOC that imports a contentservice to access a rest api.
My navigation is in the App component. The relevant part is the
<Link to="/page/123">About Page</Link>
and
<Link to="/page/456">Contact Page</Link>
When these links are clicked the page doesn't redraw as i expected. First time i go to 'About Page' it's all good. Then when i click to go to 'Contact Page' nothing changes. Then i click on the 'About Page' again and the 'Contact Page' shows.
In all the cases above the browser address bar shows the right path and if i refresh the page i go to the right page.
Here is my navigation page:
import React, { Component } from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import { connect } from "react-redux";
import Home from "./Home";
import Admin from "./Admin";
import Members from "./Members";
import Login from "./Login";
import Page from "./Page";
import PrivateRoute from "./PrivateRoute";
import "./App.css";
class App extends Component {
render() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home Page</Link>
</li>
<li>
<Link to="/page/123">About Page</Link>
</li>
<li>
<Link to="/page/456">Contact Page</Link>
</li>
<li>
<Link to="/members">Members</Link>
</li>
<li>
<Link to="/admin">Admin</Link>
</li>
</ul>
</div>
<Switch>
<Route path="/login" component={Login} />
<Route path="/page/:id" component={Page} />
<Route exact path="/" component={Home} />
<PrivateRoute path="/members">
<Members />
</PrivateRoute>
<PrivateRoute path="/admin">
<Admin />
</PrivateRoute>
</Switch>
</Router>
);
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.isLoggedIn,
};
};
export default connect(mapStateToProps, null)(App);
This is my page component:
import React, { Component } from "react";
import WithBackend from "./WithBackend";
class Page extends Component {
constructor(props) {
super(props);
this.resource = "/pages/";
this.state = { model: null };
}
render() {
if (this.state.model != null) {
return (
<div className="container">
<div className="row">
<div className="col-md">
<h1>{this.state.model.title}</h1>
<h2 dangerouslySetInnerHTML={{ __html: this.state.model.body }} />
</div>
</div>
</div>
);
} else {
return (
<div>
<h2>Page id: {this.props.match.params.id}</h2>
</div>
);
}
}
componentDidMount() {
this.props
.getEntity(this.resource, this.props.match.params.id)
.then((model) => this.setState({ model }));
}
componentDidUpdate(nextProps) {
if (nextProps.match.params.id !== this.props.match.params.id) {
this.props
.getEntity(this.resource, nextProps.match.params.id)
.then((data) => {
this.setState({ model: data });
});
}
}
}
export default WithBackend(Page);
This is the Withbackend HOC:
import React from "react";
import ContentService from "./ContentService";
const WithBackend = (WrappedComponent) => {
class HOC extends React.Component {
constructor() {
super();
this.contentService = new ContentService();
this.getEntity = this.getEntity.bind(this);
this.getEntities = this.getEntities.bind(this);
}
getEntity(resource, id) {
return this.contentService
.getEntity(resource, id)
.then((response) => response.json())
.catch((e) => {
console.log(e);
});
}
getEntities(resource) {
return this.contentService
.getEntities(resource)
.then((response) => response.json())
.catch((e) => {
console.log(e);
});
}
render() {
return (
<WrappedComponent
getEntity={this.getEntity}
getEntities={this.getEntities}
{...this.props}
/>
);
}
}
return HOC;
};
export default WithBackend;
And the content service:
class ContentService {
baseUrl = "http://localhost:8080";
getEntity(resource, id) {
const path = this.baseUrl + resource + id;
const fetchPromise = fetch(path, {
method: "GET",
});
return Promise.resolve(fetchPromise);
}
getEntities(resource) {
const fetchPromise = fetch(this.baseUrl + resource, {
method: "GET",
});
return Promise.resolve(fetchPromise);
}
}
export default ContentService;
Has anyone got any ideas why this is happening? I am not sure if it has anything to do with the Page component being wrapped by HOC but just thought it is worth mentioning.
Thank you.
Issue
The componentDidUpdate lifecycle method receives the previous props, state, and snapshot values, not the next ones.
componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
By sending the "previous" props' match param id you were a "cycle" behind.
Solution
Use the current id value from props.
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
this.props
.getEntity(this.resource, this.props.match.params.id)
.then((data) => {
this.setState({ model: data });
});
}
}

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]);
}

Passing props in the Redirect component (React.js) not working as expected

I am working through a practice app to get acquainted with React-Router
In my App component I have a Route which acts as a way to afford for 404's
class App extends Component {
render() {
return (
<Router>
<div>
<Navbar />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/players" component={Players} />
<Route path="/teams" component={Teams} />
{/* params should be last Route as it would match players and teams */}
<Route path="/:teamId" exact component={TeamPage} />
<Route
render={({ location }) => (
<h1 className="text-center">
Sorry, we couldn't find {location.pathname.substr(1)} <br />{" "}
404: page not found.
</h1>
)}
/>
</Switch>
</div>
</Router>
);
}
}
That works fine.
However I have another component TeamPage which essentially has the same mechanism.
import React, { Component } from "react";
import { Redirect, Link } from "react-router-dom";
import { getTeamNames, getTeamsArticles } from "../api";
import TeamLogo from "./TeamLogo";
import Team from "./Team";
import slug from "slug";
export default class TeamPage extends Component {
state = {
loading : true,
teamNames: {},
articles : []
};
componentDidMount() {
Promise.all([
getTeamNames(),
getTeamsArticles(this.props.match.params.teamId)
]).then(([teamNames, articles]) => {
this.setState(() => ({
teamNames,
articles,
loading: false
}));
});
}
render() {
const { loading, teamNames, articles } = this.state;
const { match } = this.props;
const { teamId } = match.params;
if (loading === false && teamNames.includes(teamId) === false) {
return (
<Redirect
to={{
pathname: "/",
location: { location: window.location.pathname.substr(1) }
}}
/>
);
}
return (
<div>
<Team id={teamId}>
{team =>
team === null ? (
<h1>LOADING</h1>
) : (
<div className="panel">
<TeamLogo id={teamId} />
<h1 className="medium-header">{team.name}</h1>
<h4 style={{ margin: 5 }}>
<Link
style = {{ cursor: "pointer" }}
to = {{ pathname: "/players", search: `?teamId=${teamId}` }}
>
View Roster
</Link>
</h4>
<h4>Championships</h4>
<ul className="championships">
{team.championships.map(ship => (
<li key={ship}>{ship}</li>
))}
</ul>
<ul className="info-list row" style={{ width: "100%" }}>
<li>
Established
<div>{team.established}</div>
</li>
<li>
Manager
<div>{team.manager}</div>
</li>
<li>
Coach
<div>{team.coach}</div>
</li>
<li>
Record
<div>
{team.wins}-{team.losses}
</div>
</li>
</ul>
<h2 className="header">Articles</h2>
<ul className="articles">
{articles.map(article => (
<li key={article.id}>
<Link to={`${match.url}/articles/${slug(article.title)}`}>
<h4 className="article-title">{article.title}</h4>
<div className="article-date">
{article.date.toLocaleDateString()}
</div>
</Link>
</li>
))}
</ul>
</div>
)
}
</Team>
</div>
);
}
}
But in this instance I only can accomplish the redirect, with no message presented in the UI.
I am trying to pass props to home or "/" so if someone did that from the TeamPage. The Route in my App component would respond with that message, like it does normally.
Excerpt from TeamPage:
const { loading, teamNames, articles } = this.state;
const { match } = this.props;
const { teamId } = match.params;
if (loading === false && teamNames.includes(teamId) === false) {
return (
<Redirect
to={{
pathname: "/",
location: { location: window.location.pathname.substr(1) }
}}
/>
);
}
Can this be done?
Thanks in advance!
Update Oct 11th 2:56PM
So as per Hannad's great insight I updated my App component and the TeamPage component and created a catch all ErrorPage route. What I need to do now is update the Teams.js file as when I try to go to http://localhost:3000/teams/foo I get the following error:
import React, { Component } from "react";
import { Redirect, Route, Link } from "react-router-dom";
import Sidebar from "./Sidebar";
import { getTeamNames } from "../api";
import TeamLogo from "./TeamLogo";
import Team from "./Team";
export default class Teams extends Component {
state = {
teamNames: [],
loading : true
};
componentDidMount() {
getTeamNames().then(teamNames => {
this.setState(() => ({
loading: false,
teamNames
}));
});
}
render() {
const { loading, teamNames } = this.state;
const { location, match } = this.props;
return (
<div className="container two-column">
<Sidebar
loading = {loading}
title = "Teams"
list = {teamNames}
{...this.props}
/>
{loading === false &&
(location.pathname === "/teams" || location.pathname === "/teams/") ? (
<div className="sidebar-instruction">Select a Team</div>
) : null}
<Route
path = {`${match.url}/:teamId`}
render = {({ match }) => (
<div className="panel">
<Team id={match.params.teamId}>
{team =>
team === null ? (
<h1>Loading</h1>
) : (
<div style={{ width: "100%" }}>
<TeamLogo id={team.id} className="center" />
<h1 className="medium-header">{team.name}</h1>
<ul className="info-list row">
<li>
Established
<div>{team.established}</div>
</li>
<li>
Manager
<div>{team.manager}</div>
</li>
<li>
Coach
<div>{team.coach}</div>
</li>
</ul>
<Link
className = "center btn-main"
to = {`/${match.params.teamId}`}
>
{team.name} Team Page
</Link>
</div>
)
}
</Team>
</div>
)}
/>
</div>
);
}
}
there is an other way to do 404 redirects. which i would suggest. as it has more control..
define your error component which takes simple params.
class App extends Component {
render() {
return (
<Router>
<div>
<Navbar />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/players" component={Players} />
<Route path="/teams" component={Teams} />
{/* params should be last Route as it would match players and teams */}
<Route path="/:teamId" exact component={TeamPage} />
<Route path="/error/:errortype" exact component={ErrorPage} />
</Switch>
</div>
</Router>
);
}
}
// your redirection logic
const { loading, teamNames, articles } = this.state;
const { match } = this.props;
const { teamId } = match.params;
if (loading === false && teamNames.includes(teamId) === false) {
return (
<Redirect
to={'/error/404'}
/>
);
}
// ErrorPage Component
const ErorPage = ({match: { params:{ errortype } }})=>(
// have your logic for different templates for different types of errors.
// ex.
<div>
{
errortype === 404 ?
<div>you got a 404</div> :
<div>sone generic message</div>
// this logic can change with respect to your app.
}
</div>
)

Resources