dynamically change the basename in react BrowserRouter with API call - reactjs

Recently in a project I've been working on had a requirement to change the content based on the user's location, so we used a third-party service to get the browsing location from IP address and according to the country they are brows from, we have to change the content and the URL of the site to something like this, domainName.com/us so the /us part should be dynamic and I used basename prop in BrowserRouter for that to maintain consistency. but the problem is I can't change the basename dynamically, since the country code comes from an API, it takes some time to fetch, and obviously, it's an async call. state variable I used is updating after data fetch, but the URL in the browser didn't update.
basically, by default, the site will load with the content according to the user's location, but if the user types let's say /uk in the URL after the domain, the site content should be changed according to that and maintain the URL in sitewide operations
here's an example site you can refer
raileurope.com
you can type, let's say as a example /fr after the domain in that site, then the site content will be changed to french, if user type /it the content will be change to italian, like wise.
a new approach to fulfilling this requirement is also welcomed. Thanks in advance.
what I have right now.
react-router-dom: 5.2.0
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const App = () => {
const [browsFrom, setBrowsFrom] = useState('');
useEffect(() => {
axios.get('https://api64.ipify.org')
.then((res) => {
axios.get(`http://www.geoplugin.net/json.gp?ip=${res.data}`)
.then((response) => {
console.log(response);
setBrowsFrom(`/${response.data.geoplugin_countryCode}`);
}).catch((e) => {
console.log(e);
setBrowsFrom('');
})
}).catch((e) => {
console.log(e);
setBrowsFrom('');
});
}, []);
return (
<Router basename={browsFrom}>
<Switch>
<Route path="/" component={HomeComponent} exact={true} />
<Route
path="/termsAndConditions"
component={TermsPage}
exact={true}
/>
<Route
path="/aboutUs"
component={AboutUsPage}
exact={true}
/>
</Switch>
</Router>
);
}

You can create a history object dynamic based on your sub-path and pass it as a history in <Router history={history}>. You can try this solution.
import React, { useState, useMemo, useEffect } from "react";
import { render } from "react-dom";
import { Router, Link } from "react-router-dom";
import { createBrowserHistory } from "history";
// To demonstrate the ability to have a dynamic
// base route, change the url path and add something
// like: "/test/123/" then refresh the page and
// navigate - then, refresh again.
const Header = () => (
<header>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/sub">Submenu</Link>
</li>
</ul>
</nav>
</header>
);
const Root = () => {
const [path, setPath] = useState("my-app");
const history = useMemo(() => {
return createBrowserHistory({ basename: path });
}, [path]);
useEffect(() => {
console.log(history);
if (path !== "my-app") {
history.replace("/");
}
}, [history, path]);
return (
<Router history={history}>
Current basename – {path}
<Header />
<button
onClick={() => {
setPath("asif");
}}
>
change base path{" "}
</button>
</Router>
);
};
render(<Root />, document.getElementById("root"));

Related

Why browser back button returns a blank page with the previous URL

On pressing the browser back button why does an empty-blank page is displayed instead of the component that I'd visited before? Only the URL is getting changed to the previous one. Using React Router v5
That is really frustrating, how can I fix this ?
SignUp.js
render() {
return (
<div className='signUp-div'>
<Header />
<Router history={history}>
<div className='form-div'>
<Redirect to='/signup/mobile' /> // Default page
<Switch>
<Route exact path={'/signup/mobile'} component={MobileNum} />
<Route exact path={'/signup/idnumber'}>
<IdentNumber setPersonalID={this.props.setUserNumber} />
</Route>
<Route exact path={'/signup/password'}>
<CreatePass
setIfSignUp={this.props.setIfSignUp}
/>
</Route>
</Switch>
</div>
</Router>
</div>
);
}
IdentNumber.js
const IdentNumber = ({setPersonalID}) => {
const handleCheckID = () => {
history.push('/signup/password');
}
return (
<div className='form-div'>
<button
onChange={(event) => onChangeHandler(event)}
> Page password</button>
</div>
);
};
export default IdentNumber;
Did I explain it right ?
Thanks
From the code sandbox link, I've observed a few things that could potentially cause this issue.
Update your imports from import { Router } from "react-router-dom";
to
import { BrowserRouter as Router } from "react-router-dom";
<BrowserRouter> uses the HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URL.
The routes will remain the same. You're using react-router-dom v5.2.0, you could use useHistory to get the history object. useHistory simplified the process of making components route-aware.
With my changes: https://codesandbox.io/s/suspicious-pine-5uwoq
We don't need exact key for all routes other than the default "/" when it is enclosed in Switch and placed in the end. But exact matches /signup/mobile and /signup/* as same. Switch renders only one route and whichever route is matched first.
An example project for reference.
And if you want to handle the back button event yourself, follow the below examples.
In a function component, we can handle the back button press by listening to the history object.
import { useHistory } from 'react-router-dom';
const Test = () => {
const history = useHistory();
useEffect(() => {
return () => {
if (history.action === "POP") {
}
};
}, [history])
}
listen to history in useEffect to find out if the component is unmounted. history.listen lets us listen for changes to history.
Example:
import { useHistory } from 'react-router-dom';
const Test = () => {
const history = useHistory();
useEffect(() => {
return history.listen(location => {
if (history.action === 'POP') {
}
})
}, [])
}
react-router-dom now has Prompt,
import { Prompt } from "react-router-dom";
<Prompt
message={(location, action) => {
if (action === 'POP') {
// back button pressed
}
return location.pathname.startsWith("/test")
? true
: `Are you sure you want to go to ${location.pathname}?`
}}
/>

How do I navigate to another page?

I take data from the resource thememoviedb. I received a certain number of movies, and I also displayed them on the page. I try to go to each movie to see the details in the file Movie_Details.js. In this file, I get data from the resource as well, but the transition does not occur, and the Movie_Details component is drawn at the bottom. How do I go to the Movie Details page and display the data? Thank you
App.js
import {BrowserRouter as Router, Switch, Route, Link} from "react-router-dom";
import {Popular_Movies} from "./Components/Popular_Movies/Popular_Movies";
import {Movie_Detail} from "./Components/Popular_Movies/Movie_Detail";
function App() {
return (
<div>
<Router>
<Popular_Movies/>
<Route path="/movie/:id" exact>
<Movie_Detail/>
</Route>
</Router>
</div>
);}
export default App;
Popular_Movie.js
import React from "react";
import {image_api} from "../../Services/Service";
import {Link} from "react-router-dom";
export let Popular_Movie = ({name, title, poster_path, vote_average}) => {
return (<div className='different_movie'>
<img src={image_api + poster_path} className='image'/>
<div className='bottom_different_movie'>
<h3> <Link to={`/movie/:id`}>{name} {title} {vote_average}</Link></h3>
</div>
</div>) }
Popular_Movies.js
import React, {useEffect, useState} from "react";
import {Popular_Movie} from "./Popular_Movie";
export let Popular_Movies = () => {
let popularMovieUrl = `https://api.themoviedb.org/3/tv/popular?api_key=e12a044061e5fe9077c7aee8a5165126&language=en-US&page=1`;
let fetchMovies = async () => {
let response = await fetch(popularMovieUrl)
let data = await response.json()
setPopularMovie(data.results)
}
useEffect(() => {
fetchMovies()
},[])
return (<div>
{popularMovie.length > 0 && popularMovie.map(movie => <Popular_Movie key={movie.id} {...movie}/>)}
</div>) }
Movie_Detail
import React, {useEffect, useState} from "react";
export let Movie_Detail = () => {
let movieUrl = 'https://api.themoviedb.org/3/tv/popular?api_key=e12a044061e5fe9077c7aee8a5165126&language=en-US&page=1'
let [differentMovie, setDifferentMovie] = useState([])
let fetchDifferentMovie = async () => {
let response = await fetch(movieUrl)
let data = await response.json()
setDifferentMovie(data.results)
}
useEffect(() => {
fetchDifferentMovie()
},[])
return (<div>
{differentMovie.title}
</div>)}
If you want to show movie list on / route, and movie details on /movie/:id you should have your router structured like this:
<Router>
... // put navigation component here
<Switch>
<Route path="/" exact>
<Popular_Movies/>
</Route>
<Route path="/movie/:id" exact>
<Movie_Detail/>
</Route>
</Switch>
</Router>
Inside Movie_Detail component you need to extract movie id by using the useParams hook
const { id } = useParams();
... // fetch movie details using `id`
It's important for you to understand how Switch operates. It's like you have if-else logic which shoud decide which component to render when certain path is matched. Please, read more on Basic Routing.
P.S. The reason you're seeing both popular movies and movie details underneath is because your Popular_Movies component call was not wrapped with <Route> which means that it should be always present/visible.

Using useHistory on React

I am a beginner in React and trying to learn things by myself. I have this code that I'd like to navigate to the Login page using useHistory but I can't seem to make it work. Hope you can help me. Here is my code below:
import { useHistory } from "react-router-dom";
const App = () => {
let history = useHistory();
const MoveToLogin = () => {
history.push('./container/Login');
}
return (
<div>
<button className='btn' text='User Login' onClick=.
{MoveToLogin}>Login</button>
</div>
);
}
export default App;
First you need to add Provider 'Router' which is imported from 'react-router-dom'
Define routes and corresponding components in routing file.
You can only use history inside the children of Provider Router.
Navigate to route using history.push('/login'). Don't provide relative path of component file here. Use the route you want to show in browser url
I have played around and did more research about useHistory. I was able to make it work. It has navigated to a new component. Please find my solution below. Hope it can help others with the same kind of issue.
import UserLogin from "./pages/UserLogin";
const ButtonLogin = () => {
let history = useHistory();
const MoveToLogin = () => {
history.push('/pages/UserLogin');
}
return (
<div><button className='btn btn-primary' onClick={MoveToLogin}>Login</button></div>
);
}
const App = () => {
return (
<div>
<Router>
<Route path="/pages/UserLogin" exact component={UserLogin} />
<Route path="/" exact component={ButtonLogin} />
</Router>
</div>
);
}
export default App;
Refer to this answer as well for further information.
Cannot read property 'push' of undefined for react use history
I hope you have the routes defined for the path specified
<Router>
<Route path="/container/Login" exact component={LoginComponent} />
</Router>
The dot is not needed in your move to login function.
import { useHistory } from "react-router-dom";
const App = () => {
let history = useHistory();
const MoveToLogin = () => {
history.push('/container/Login'); // Here you don't need dot ./container
}
return (
<div>
<button className='btn' text='User Login' onClick=.
{MoveToLogin}>Login</button>
</div>
);
}
export default App;

How to map a filter of a string value in an array of strings

I have an app built with React and Express Node, and in it I have 3 separate components. The first component is a gallery, where user selects an image to create a post with an background image. When button is clicked, user is taken to a form. The user will edit the inputs with some text and save the form which has a axios.post request to send the data to mongo db through express route. After saving user clicks view post that takes them to another component with axios.get request displaying image and input data to the user.
I have routes that have a unique http path to show the component that is active. My question is how can I map the routes to dynamically load the name of the image that comes from mongodb collection, instead of manually writing in the paths image name ie: path={"/getinputwaterfall/:id"}, path={"/getinputcross/:id"}, path={"/getinputfreedom/:id"} . I would like to have instead somthing like: path={"/getinput{urlName}/:id"}.
In the mongoDB collection I have a URL and name string array. The URL string is an http path from firebase and the name string are images names.
Is this possible to do?
Bellow is the code and my attempts to do this.
import React, {useState, useEffect} from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import axios from "axios";
import "bootstrap/dist/css/bootstrap.min.css";
import "./index.css";
//gallery imports
import Cross from "./Components/Gallery/posts/Cross";
import Waterfall from "./Components/Gallery/posts/Waterfall";
import Freedom from "./Components/Gallery/posts/Freedom";
import CrossPost from "./Components/Gallery/get/CrossPost";
import WaterfallPost from "./Components/Gallery/get/WaterfallPost";
import FreedomPost from "./Components/Gallery/get/FreedomPost";
function App() {
const [name, setName] = useState([]);
const loadImage = async () => {
try {
let res = await axios.get("http://localhost:5000/geturls");
console.log(res.data)
setName(res.data.map(n=>n.name)); //array of names
} catch (error) {
console.log(error);
}
};
useEffect(() => {
loadImage();
}
,[]);
return (
<div className="App">
<Router>
<Switch>
{/* routes for gallery */}
<Route path={"/waterfall"} component={Waterfall} />
<Route path={"/cross"} component={Cross} />
<Route path={"/freedom"} component={Freedom} />
<Route path={"/getinputwaterfall/:id"} component={WaterfallPost} />
<Route path={"/getinputcross/:id"} component={CrossPost} />
<Route path={"/getinputfreedom/:id"} component={FreedomPost} />
{/* what I tryed to map */}
{name.filter(name => name === `${name}` ).map((urlName) => (
<Route exact path={`/getinputt/${urlName}`} component={CrossPost} />
))}
</Switch>
</Router>
</div>
);
}
export default App;
update: I applied the first option of the answer to my code for those who wish to see the complete solution: Note: I had to remove the '/' in /getinput/${name}/:id to make my code work! Thanks Drew!
const imagePostRoutes = [
{ name: "cross", component: CrossPost },
{ name: "freedom", component: FreedomPost },
{ name: "waterfall", component: WaterfallPost },
];
return (
<div className="App">
<Router>
<Switch>
{imagePostRoutes.map(({ component, name }) => (
<Route
key={name} path={`/getinput${name}/:id`} component={component}
/>
))}
</Switch>
</Router>
</div>
);
}
I was first suggesting to create a "routes" config array that can be mapped.
const imagePostRoutes = [
{ name: "cross", component: CrossPost },
{ name: "freedom", component: FreedomPost },
{ name: "waterfall", component: WaterfallPost },
];
...
{imagePostRoutes.map(({ component, name }) => (
<Route key={name} path={`/getInput/${name}/:id`} component={component} />
))}
The second suggestion is to use a single generic dynamic route where a match parameter could specify the post type and a general post component to render the specific image post component. This is a very stripped down minimal version.
Define the route.
<Route path="/getInput/:imagePostType/:id" component={ImagePost} />
Create a Map of match param to component to render.
const postComponents = {
cross: CrossPost,
freedom: FreedomPost,
waterfall: WaterfallPost,
};
Create a component to read the match params and load the correct post component from the Map.
const ImagePost = () => {
const { id, imagePostType } = useParams();
const Component = postComponents[imagePostType];
if (!Component) {
return "Unsupported Image Post Type";
}
return <Component id={id} />;
}

react-router#5 route `onEnter` method not calling, scrolltoview not working

according to my requirement, when a user click on
<Link to="/products/shoe#product9">Go to projects and focus id 9</Link> I would like to show him the product. (hello page) for that I do this:
import React from "react";
import { Link, Route, Switch, Redirect } from "react-router-dom";
import "./products.scss";
const Shoes = React.lazy(() => import("./shoes/shoes.component"));
const Cloths = React.lazy(() => import("./cloths/cloths.component"));
function hashScroll() {
alert("called");
const { hash } = window.location;
if (hash !== "") {
setTimeout(() => {
const id = hash.replace("#", "");
const element = document.getElementById(id);
if (element) element.scrollIntoView();
}, 0);
}
}
export default class Products extends React.Component {
render() {
return (
<div>
<header>
<Link to="/products/shoe">Shoes</Link>
<Link to="/products/cloths">Cloths</Link>
</header>
<h1>Products page</h1>
<main>
<Switch>
<Redirect exact from="/products" to="/products/shoe" />
<Route path="/products/shoe" onEnter={hashScroll}>
<Shoes />
</Route>
<Route path="/products/cloths">
<Cloths />
</Route>
</Switch>
</main>
</div>
);
}
}
I attached an onEnter function to call and scroll, so when there is a #hash let it scroll. It's not working at all. Please navigate to Hello page, from you click the link to go to products page.
Live Demo
onEnter is no longer working in react-router
What you can do is pass a prop to the component
<Shoes onEnter={hashScroll} />
inside the Shoes component execute it on componentDidMount.
componentDidMount = () => {
if (this.props.onEnter) {
this.props.onEnter();
}
};
demo

Resources