How to set properly react-router-dom links with React - reactjs

I've been struggling with my first solo proyect due to my lack of experience, here is the trouble I have:
I need to set a Link component (of react-router-dom) which takes me to another new page when I clicked the button. Thought that selection I need to pass to the link (and the context) the id so I can fetch the data from the API, but I cannot get an idea of how to make it works. This is the button link component:
Model.jsx
import React, { useState, useContext } from 'react';
import { BrowserRouter as Router, Switch, Route, Link, Redirect } from 'react-router-dom';
import FileModel from "../FileModel/FileModel";
import { ModelsContext } from "../../context/ModelsContext";
const Model = ({modelo}) => {
const { id, name, year, price, photo } = modelo;
const { guardarModelo } = useContext(ModelsContext);
const [display, setDisplay] = useState("btn-notdisplayed");
const showButton = e => {
e.preventDefault();
setDisplay("btn-displayed");
};
const hideButton = e => {
e.preventDefault();
setDisplay("btn-notdisplayed");
};
return (
<div
className="card"
onMouseEnter={e => showButton(e)}
onMouseLeave={e => hideButton(e)}
>
<div className="card-body">
<p className="card-name">{name}</p>
<p className="card-yearprice">{year} | $ {price}</p>
</div>
<img src={`https://challenge.agenciaego.tech${photo}`} className="card-image" alt={`Imagen de ${name}`} />
<Router>
<button
type="button"
className={display}
onClick={() => {
guardarModelo(modelo);
}}
><Link to={`/models/${modelo.id}`}>Ver Modelo</Link>
</button>
<Switch>
<Route exact path={`/models/${modelo.id}`} component={FileModel} />
</Switch>
</Router>
</div>
);
}
export default Model;
Then I obtained the data from a context:
ModelsContext.jsx
import React, { createContext, useState, useEffect } from 'react';
export const ModelsContext = createContext();
const ModelsProvider = (props) => {
//State de modelos
const [ modelo, guardarModelo ] = useState({});
const [ modelos, guardarModelos ] = useState([]);
const [ allModelos, guardarAllModelo ] = useState([]);
//Cargar un modelo
useEffect(() => {
const consultarAPI = async () => {
const api = await fetch("https://challenge.agenciaego.tech/models");
const modelos = await api.json();
const api2 = await fetch(`https://challenge.agenciaego.tech/models/${id}`);
const modelo = await api2.json();
guardarAllModelo(modelos);
guardarModelos(modelos);
guardarModelo(modelo);
}
consultarAPI()
}, []);
return (
<ModelsContext.Provider
value={{
allModelos,
modelo,
modelos,
guardarModelo,
guardarModelos
}}
>
{props.children}
</ModelsContext.Provider>
)
}
export default ModelsProvider;
Finally, I got the App.js from which I route the principal component, the idea Is to get with the Link to the new component called "FileModel.jsx" as a child component an so maintains the Navbar component.
App.js
import React from "react";
import Navbar from "./components/Nav/Navbar";
import Models from "./components/Models/Models";
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Logo from "./assets/img/logo.png";
import ModelsProvider from "./context/ModelsContext";
import ModelFooter from "./components/Models/ModelFooter";
function App() {
return (
<ModelsProvider>
<Router>
<nav className="navbar">
<img src={Logo} className="logo" alt="Ego Logo" />
<div className="menu-container">
<Link to={'/models'} className="menu-items">Modelos</Link>
<a className="menu-items">Ficha de modelo</a>
</div>
<div className="bottom-line"></div>
</nav>
<Switch>
<Route exact path='/models' component={Models} />
</Switch>
</Router>
<Navbar />
<ModelFooter />
</ModelsProvider>
);
}
export default App;
This is the fileModel.jsx so far:
import React, { useContext } from 'react';
import Navbar from "../Nav/Navbar";
import { ModelsContext } from "../../context/ModelsContext";
const FileModel = () => {
const { modelo } = useContext(ModelsContext);
console.log(modelo.id);
return (
<Navbar />
);
}
export default FileModel;
I hope to have explained my issue clear, and thanks so much to all you caring people!
Cheers!
Ps: Maybe you can find some things to refactor (I will need to check my code later), If you find something like that any help will be appreciated!
UPDATE
Due to Linda recommendation I merged the two contexts into one, and changed some lines of the code I wrote before, I cannot set the state to pass the solo model to the fileModel component and the Link still not working, I was thinking in a functions that can do it, I made another state, a single modelo, but when I click the button I got an error and undefined, because Idk how to set in the state the Id and so pass it to the API call, the terminal says that id in const api2 = await fetch(https://challenge.agenciaego.tech/models/${id}); is not defined.

I FINALLY SOLVED MY PROBLEM!!! I had to rellocate some elements between my component to make the api context take the id and pass it to the FileModel component, and change the Link router to the App.js, this is how I get to the solution:
App.js
import React from "react";
import Navbar from "./components/Nav/Navbar";
import Models from "./components/Models/Models";
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Logo from "./assets/img/logo.png";
import ModelsProvider from "./context/ModelsContext";
import ModelFooter from "./components/Models/ModelFooter";
import FileModel from "./components/FileModel/FileModel";
function App() {
return (
<ModelsProvider>
<Router>
<nav className="navbar">
<img src={Logo} className="logo" alt="Ego Logo" />
<div className="menu-container">
<Link to={'/models'} className="menu-items">Modelos</Link>
<Link to={`/models/1`} className="menu-items">Ficha del Modelo</Link>
</div>
<div className="bottom-line"></div>
</nav>
<Switch>
<Route exact path='/models' component={Models} />
<Route exact path='/models' component={Models} />
</Switch>
</Router>
<Navbar />
<FileModel />
<ModelFooter />
</ModelsProvider>
);
}
export default App;
Modelscontext.jsx
import React, { createContext, useState, useEffect } from 'react';
export const ModelsContext = createContext();
const ModelsProvider = (props) => {
//State de modelos
const [ modelo, guardarModelo ] = useState([]);
const [ modelos, guardarModelos ] = useState([]);
const [ allModelos, guardarAllModelo ] = useState([]);
const { id } = modelo;
//Cargar un modelo
useEffect(() => {
const consultarAPI = async () => {
const api = await fetch("https://challenge.agenciaego.tech/models");
const modelos = await api.json();
const api2 = await fetch(`https://challenge.agenciaego.tech/models/${id}`);
const modelo = await api2.json();
guardarAllModelo(modelos);
guardarModelos(modelos);
guardarModelo(modelo);
}
consultarAPI()
}, [modelo.id]);
return (
<ModelsContext.Provider
value={{
allModelos,
modelo,
modelos,
guardarModelo,
guardarModelos
}}
>
{props.children}
</ModelsContext.Provider>
)
}
export default ModelsProvider;
Model.js
import React, { useState, useContext } from 'react';
import { Link } from 'react-router-dom';
import { ModelsContext } from "../../context/ModelsContext";
const Model = ({modelo}) => {
const { name, year, price, photo } = modelo;
const { guardarModelo } = useContext(ModelsContext);
const [display, setDisplay] = useState("btn-notdisplayed");
const showButton = e => {
e.preventDefault();
setDisplay("btn-displayed");
};
const hideButton = e => {
e.preventDefault();
setDisplay("btn-notdisplayed");
};
return (
<div
className="card"
onMouseEnter={e => showButton(e)}
onMouseLeave={e => hideButton(e)}
>
<div className="card-body">
<p className="card-name">{name}</p>
<p className="card-yearprice">{year} | $ {price}</p>
</div>
<img src={`https://challenge.agenciaego.tech${photo}`} className="card-image" alt={`Imagen de ${name}`} />
<Link to={`/models/${modelo.id}`}><button
type="button"
className={display}
onClick={() => {
guardarModelo(modelo);
}}
>Ver Modelo
</button></Link>
</div>
);
}
export default Model;
FileModel.js (so far...)
import React, { useContext } from 'react';
import Navbar from "../Nav/Navbar";
import { ModelsContext } from "../../context/ModelsContext";
const FileModel = () => {
const { modelo } = useContext(ModelsContext);
const { photo, name } = modelo;
return (
<>
<Navbar />
<section>
<img src={`https://challenge.agenciaego.tech${photo}`} alt={`Imagen de ${name}`}/>
</section>
</>
);
}
export default FileModel;
Thanks to Linda for giving me the input to refactoring the context! cheers!

Related

Whenever I Put the payment page on my app, it goes blank on ReactJS

I'm doing an e-commerce app in which you rent tuxedos but when I go to the payments page it goes blank. It happened when I installed Stripe API on my app and it became buggy in the specific page. In this version of React, I tried to put on the payment page but it goes blank. Can you guys help me solve this problem please?
Here's my code on App.js:
import './App.css';
import Header from './Header.js';
import Home from './Home.js';
import Checkout from './Checkout.js';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Login from './Login';
import { useEffect } from 'react';
import { auth } from './firebase';
import { useStateValue } from './StateProvider';
import Payment from './Payment';
import { loadStripe } from '#stripe/stripe-js';
import { Elements } from '#stripe/react-stripe-js';
const promise = loadStripe('some stripe api here');
function App() {
const [{}, dispatch] =useStateValue();
useEffect(() => {
//Only run once the app component logs
auth.onAuthStateChanged(authUser => {
console.log('User is signed in', authUser)
if (authUser) {
dispatch({
type:'SET_USER',
user: authUser
})
} else {
dispatch({
type:'SET_USER',
user: null
})
}
})
}, [])
return (
//BEM
<Router>
<div className="app">
<Routes>
<Route path="/login" element={[<Login />]}/>
<Route path="/checkout" element={[<Header />, <Checkout />]}/>
<Route path="/payment" element={[<Header />, <Elements stripe={promise} />, <Payment />]}/>
<Route path="/" element={[<Header />, <Home />]}/>
</Routes>
</div>
</Router>
);
}
export default App;
Now here's my code on the Payment page (Payment.js):
import { CardElement, useElements, useStripe } from '#stripe/react-stripe-js';
import React, { useEffect, useState } from 'react';
import CurrencyFormat from 'react-currency-format';
import { Link, useNavigate } from 'react-router-dom';
import CheckoutProduct from './CheckoutProduct';
import './Payment.css';
import { useStateValue } from './StateProvider';
import { getCartTotal } from './reducer';
import axios from 'axios';
function Payment() {
const [{cart, user}, dispatch] = useStateValue();
const navigate = useNavigate();
const stripe = useStripe();
const elements = useElements();
const [succeeded, setSucceeded] = useState(false);
const [processing, setProcessing] = useState("");
const [error, setError] = useState(null);
const [disabled, setDisabled] = useState(true);
const [clientSecret, setClientSecret] = useState(true);
useEffect(() => {
const getClientSecret = async() => {
const response = await axios({
method: 'post',
url: `/payments/create?total=${getCartTotal(cart) * 100}`
});
setClientSecret(response.data.clientSecret)
}
getClientSecret();
}, [cart])
const handleSubmit = async(event) => {
event.preventDefault();
setProcessing(true);
const payload = await stripe.confirmCardPayment(clientSecret, {
payment_method : {
card: elements.getElement(CardElement)
}
}).then(({paymentIntent}) => {
setSucceeded(true);
setError(null)
setProcessing(false)
navigate('/orders', {replace:true});
})
}
const handleChange = event => {
setDisabled(event.empty);
setError(event.error ? event.error.message : '');
}
return (
<div className='payment'>
<div className='payment_container'>
<h1> Checkout (<Link to='/checkout'> {cart?.length} items </Link>) </h1>
{/* Payment section - Delivery address */}
<div className='payment_section'>
<div className='payment_title'>
<h3> Delivery Address </h3>
</div>
<div className='payment_address'>
<p> {user?.email} </p>
<p> 123 Elvis Lane </p>
<p> Austin, Texas </p>
</div>
</div>
{/* Payment section - Review items */}
<div className='payment_section'>
<div className='payment_title'>
<h3> Review items and delivery </h3>
<div className='payment_items'>
{cart.map(item => (
<CheckoutProduct
id = {item.id}
title = {item.title}
image = {item.image}
price = {item.price}
rating = {item.rating}
/>
))}
</div>
</div>
</div>
{/* Payment section - Payment method */}
<div className='payment_section'>
<div className='payment_title'>
<h3> Payment Method </h3>
<div className='payment_details'>
{/* Stripe API */}
<form onSubmit={handleSubmit}>
<CardElement onChange={handleChange} />
<div className='payment_priceContainer'>
<CurrencyFormat
renderText={(value) => (
<>
<h3> Order Total: {value} </h3>
</>
)}
decimalScale={2}
value= {getCartTotal(cart)}
displayType={"text"}
thousandSeparator={true}
prefix={"$"}
/>
<button disabled={processing || disabled || succeeded}>
<span> {processing ? <p> Processing </p> : "Buy Now"} </span>
</button>
</div>
{error && <div>{error}</div>}
</form>
</div>
</div>
</div>
</div>
</div>
)
}
export default Payment
Is this an error on App.js or is it in Payment.js? The page should display the info and the payment form.
Edit: I found out it was in the Payment.js code somewhere around here:
const navigate = useNavigate();
const stripe = useStripe();
const elements = useElements();
const [succeeded, setSucceeded] = useState(false);
const [processing, setProcessing] = useState("");
const [error, setError] = useState(null);
const [disabled, setDisabled] = useState(true);
const [clientSecret, setClientSecret] = useState(true);
useEffect(() => {
const getClientSecret = async() => {
const response = await axios({
method: 'post',
url: `/payments/create?total=${getCartTotal(cart) * 100}`
});
setClientSecret(response.data.clientSecret)
}
getClientSecret();
}, [cart])
const handleSubmit = async(event) => {
event.preventDefault();
setProcessing(true);
const payload = await stripe.confirmCardPayment(clientSecret, {
payment_method : {
card: elements.getElement(CardElement)
}
}).then(({paymentIntent}) => {
setSucceeded(true);
setError(null)
setProcessing(false)
navigate('/orders', {replace:true});
})
}
const handleChange = event => {
setDisabled(event.empty);
setError(event.error ? event.error.message : '');
Can you guys help me fix this please? It seems that in this section is where the error is occurring.
Edit 2:
Here's of how it should look like:
Here's what actually happens:
Edit 3: Here's what the console gives me as an error, maymbe it is in the elements tag that causes the problem.
It looks like you need to wrap your checkout page in an Elements provider:
To use the Payment Element component, wrap your checkout page component in an Elements provider. Call loadStripe with your publishable key, and pass the returned Promise to the Elements provider. Also pass the client secret from the previous step as options to the Elements provider.
The sample code Stripe provides shows how to properly structure your app:
import React from 'react';
import ReactDOM from 'react-dom';
import {Elements} from '#stripe/react-stripe-js';
import {loadStripe} from '#stripe/stripe-js';
import CheckoutForm from './CheckoutForm';
// Make sure to call `loadStripe` outside of a component’s render to avoid
// recreating the `Stripe` object on every render.
const stripePromise = loadStripe('pk_test_123');
function App() {
const options = {
// passing the client secret obtained in step 2
clientSecret: '{{CLIENT_SECRET}}',
// Fully customizable with appearance API.
appearance: {/*...*/},
};
return (
<Elements stripe={stripePromise} options={options}>
<CheckoutForm />
</Elements>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
import React from 'react';
import {PaymentElement} from '#stripe/react-stripe-js';
const CheckoutForm = () => {
return (
<form>
<PaymentElement />
<button>Submit</button>
</form>
);
};
export default CheckoutForm;

How to fix a Spinner behaviour in a React project to wait img rendering

I have a React project deployed at this link: https://musing-hermann-5129b4.netlify.app/models
Recently I added an spinner at the beginning of the rendering, but I didn't find a way to wait that the main page is rendered, so I need to change the spinner behaviour, so it can stop when all the main page finish rendering, specially the images, which take time to load due to the fetch api:
Here is the code of the app.js
import React, { useState, useEffect} from 'react';
import ModelFooter from "./components/Models/ModelFooter";
import FileModel from "./components/FileModel/FileModel";
import Navbar from "./components/Nav/Navbar";
import Models from "./components/Models/Models";
import { BrowserRouter as Router, Switch, Route, Link, Redirect } from 'react-router-dom';
import Logo from "./assets/img/logo.png";
import ModelsProvider from "./context/ModelsContext";
import Spinner from "./components/Spinner";
function App() {
const [ loading, setLoading ] = useState(false);
useEffect(() => {
setLoading(true);
setTimeout(() => {
setLoading(false);
}, 4000);
}, [])
return (
<ModelsProvider>
{ loading ?
<Spinner loading={loading} />
:
<main>
<Router>
<div className="content-container">
<nav className="navbar">
<img src={Logo} className="logo" alt="Ego Logo" />
<div className="menu-container">
<Link to={'/models'} className="menu-items">Modelos</Link>
<Link to={`/models/:id`} className="menu-items">Ficha del Modelo</Link>
</div>
<div className="bottom-line"></div>
</nav>
<Switch>
<Route exact path="/"><Redirect to="/models"/></Route>
<Route exact path='/models' component={Models} />
<Route exact path='/models/:id' component={FileModel} />
</Switch>
<Navbar />
</div>
<ModelFooter />
</Router>
</main>
}
</ModelsProvider>
);
}
export default App;
And here is the ModelsContext.jsx where the app.js fetchs the api data:
import React, { createContext, useState, useEffect } from 'react';
export const ModelsContext = createContext();
const ModelsProvider = (props) => {
//State de modelos
const [ modelo, guardarModelo ] = useState([]);
const [ modelos, guardarModelos ] = useState([]);
const [ allModelos, guardarAllModelo ] = useState([]);
const { id } = modelo;
//Cargar un modelo
useEffect(() => {
const consultarAPI = async () => {
try {
const api = await fetch("https://challenge.agenciaego.tech/models");
const modelos = await api.json();
const api2 = await fetch(`https://challenge.agenciaego.tech/models/${id ? id : ""}`);
const modelo = await api2.json();
guardarAllModelo(modelos);
guardarModelos(modelos);
guardarModelo(modelo);
} catch (error) {
console.log(error);
}
}
consultarAPI()
}, [id]);
return (
<ModelsContext.Provider
value={{
allModelos,
modelo,
modelos,
guardarModelo,
guardarModelos
}}
>
{props.children}
</ModelsContext.Provider>
)
}
export default ModelsProvider;
I hope I'd had being clear with my explanation, thanks so much in advance!

Warning: Expected `onClick` listener to be a function, instead got a value of `boolean` type

I am working with react. When I try to play audio from a button I am receiving this error. Everything worked before when the website was only a one page website. Then I turned it into a multi-page website using react-router-dom router, switch, and route everything started to error out with the onClick. I am not sure what went wrong or how to fix it. I am still pretty new with react. Here is the code:
Player.js
import React, { useState } from "react";
import "./Button.css";
import {useTranslation} from "react-i18next";
import teaser from '../sounds/teaser-final.mp3';
const Player = ({ url }) => {
const useAudio = url => {
const [audio] = useState(new Audio(teaser));
const [playing, setPlaying] = useState(true); //nothing is playing on default
const toggle = () => {
setPlaying(!playing);
playing ? audio.play() : audio.pause()
console.log("audio is playing" + toggle);
};
return [playing, toggle];
};
const [toggle] = useAudio(url);
const {t} = useTranslation('common');
return (
<div>
<audio id="player" style={{'display': 'none'}} src={teaser}></audio>
<button
className="btns hero-button btn--outline btn--large"
onClick={toggle}
>
{t('heroSection.button')}
</button>
</div>
);
};
export default Player;
HeroSection.js (file where Player.js is used)
import React from 'react'
import './HeroSection.css'
import Player from '../Player';
function HeroSection() {
return (
<div className="hero-btns">
<Player />
</div>
)
}
export default HeroSection;
App.js
import React, { useState, useEffect } from 'react';
import './index.css';
import {BrowserRouter, Switch, Route} from 'react-router-dom';
import Axios from 'axios';
import AdminHome from './Components/auth/Admin';
import Login from './Components/auth/Login';
import Register from './Components/auth/Register';
import UserContext from './Context/UserContext';
import Navbar from './Components/Navbar/Navbar';
import HeroSection from './Components/HeroSection/HeroSection';
import ShareSection from './Components/ShareSection/ShareSection';
import Subscribe from './Components/Subscribe/Subscribe';
import Footer from './Components/Footer/Footer';
import About from './Components/About/About';
import TheApp from './Components/TheApp/TheApp';
import Contact from './Components/SocialSection/Contact';
import CookiesPopUp from './Components/Cookies/CookiesPopUp';
function Home() {
return (
<>
<CookiesPopUp />
<Navbar />
<HeroSection />
<ShareSection />
<Subscribe />
<TheApp />
<About />
<Contact />
<Footer />
</>
);
};
function App() {
const [userData, setUserData] = useState({
token: undefined,
user: undefined,
});
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
const checkedLoggedIn = async () => {
let token = localStorage.getItem("auth-token");
if(token === null) {
localStorage.setItem("auth-token", "");
token = "";
}
const tokenRes = await Axios.post(
"http://localhost:5000/users/tokenIsValid", null,
{headers: { "x-auth-token": token }}
);
if (tokenRes.data) {
const userRes = await Axios.get("http://localhost:5000/users/", {
headers: { "x-auth-token": token },
});
setUserData({
token,
user: userRes.data,
});
}
return () => setDidMount(false);
};
checkedLoggedIn();
}, [])
if(!didMount) {
return null;
}
return (
<>
<BrowserRouter>
<UserContext.Provider value={{userData, setUserData}}>
<Switch>
<Route path="/" component={Home} exact />
<Route path="/admin" component={AdminHome} exact />
<Route path="/admin/login" component={Login} exact />
<Route path="/admin/register" component={Register} exact />
</Switch>
</UserContext.Provider>
</BrowserRouter>
</>
);
}
export default App;
And this is the error I get in console when I try to play the audio.
You are overriding the definition of toggle with this code :
const [toggle] = useAudio(url);. The Player.js has multiple declarations and definitions of toggle. See:
const toggle = () => {
setPlaying(!playing);
playing ? audio.play() : audio.pause()
console.log("audio is playing" + toggle);
};
return [playing, toggle];
};
..
...
..
const [toggle] = useAudio(url);
Hence the Error Expected OnClick to be a function but provided a boolean
Thank you for your feedback! I managed to get everything to work by changing my code in Player.js to:
import React, { useState } from "react";
import "./Button.css";
import {useTranslation} from "react-i18next";
import teaser from '../sounds/teaser-final.mp3';
const Player = () => {
const [playing, setPlaying] = useState(true);
const [audio] = useState(new Audio(teaser));
const toggle = () => {
setPlaying(!playing);
playing ? audio.play() : audio.pause()
};
const {t} = useTranslation('common');
return (
<div>
<audio id="player" style={{'display': 'none'}} src={teaser}></audio>
<button
className="btns hero-button btn--outline btn--large"
onClick={toggle}
>
{t('heroSection.button')}
</button>
</div>
);
};
export default Player;

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