hide id or query string while passing through react router - reactjs

i am having route where i pass id,but i dont want to show id in url,
`<Route path={`${match.url}invite-members/:groupID`} exact component={InviteMembers} />`
this gets converted in url https://local..../invite-members/5,
but instead of that i want https://local..../invite-members, but the functionality should remain the same as in i get id in invite-members through this.props.match.params.groupID should be as it is,please help
using react router "react-router-dom": "^4.2.2",

If you want to change url to '/invite-members', you can add the Redirect component. And in case you want to save groupId, you could save it to your component state:
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import {
Router,
Route,
Link,
Switch,
Redirect
} from "react-router-dom";
class Root extends PureComponent {
// add groupId field to your component
// In case you use redux or any another state management library, you can save groupId to store
state = { groupId: null };
render() {
const { store, history } = this.props;
// just for example I defined '/' path to redirect on /invite-members url
return (
<Router>
<Switch>
<Route
path="/"
exact
render={props => (
<Redirect
to={{
pathname: "/invite-members/123",
state: { from: props.location }
}}
/>
)}
/>
<Route
path="/invite-members"
exact
render={props => (
<InviteMembers {...props} groupId={this.state.groupId} />
)}
/>
<Route
path="/invite-members/:groupID"
exact
render={props => {
return (
<RedirectAndSaveGroupId
{...props}
groupId={props.match.params.groupID}
onSetGroupId={groupId => {
this.setState({ groupId });
}}
/>
);
}}
/>
</Switch>
</Router>
);
}
}
export default Root;
class RedirectAndSaveGroupId extends PureComponent {
componentDidMount() {
// save groupId to Root component
this.props.onSetGroupId(this.props.groupId);
}
render() {
// redirect to /invite-members without groupId
return (
<Redirect
to={{
pathname: "/invite-members",
state: { from: this.props.location }
}}
/>
);
}
}
// Just for demo. In this.props.groupId we can receive groupId
class InviteMembers extends PureComponent {
render() {
return this.props.groupId;
}
}
Note, that in case you using any state management library such as Redux, you can store group id in them

I maybe have a very simple solution :
Router link :
<Link to={{pathname: '/item/'+name, state : {id}}}>{name}</Link>
In the Targeted file :
state = this.props.location.state
QueryParameters = () => {
const id = this.state.id
return { id }
}
And launch your query requiring the ID. It does not appear in the url.

Passing data in the params object will always result in that data being shown in the URL. Because the params object is built from the url.

Related

React router is not mounting the component

I'm using React Router for routing to different routes as below:
<Router>
<Switch>
<Route path="/teams/:teamName/matches" >
<MatchPage/>
</Route>
<Route path="/teams/:teamName" >
<TeamPage/>
</Route>
</Switch>
</Router>
Now in my TeamPage component I'm calling an API using async and then in the render method invoking another component called MatchDetailCard
class TeamPage extends Component {
constructor(props) {
console.log('const called')
super(props)
this.state = {
team: [],
teamName:null
}
}
async componentDidMount() {
console.log(this.props.match.params.teamName);
const teamName = this.props.match.params.teamName;
const response = await fetch(`http://localhost:8080/team/${teamName}`);
const json = await response.json();
console.log(json);
this.setState({team:json, teamName: teamName});
}
componentDidUpdate () {
console.log('updated')
}
render() {
if (!this.state.team || !this.state.team.teamName) {
return <h1>Team not found</h1>;
}
return (
<div className="TeamPage">
<div className="match-detail-section">
<h3>Latest Matches</h3>
<MatchDetailCard teamName={this.state.team.teamName} match={this.state.team.matches[0]}/>
</div>
</div>
);
}
}
export default withRouter(TeamPage);
Within the MatchDetailCard component I create a router Link to the same TeamPage component which will have a different teamName this time as below:
const MatchDetailCard = (props) => {
if (!props.match) return null;
const otherTeam = props.match.team1 === props.teamName ? props.match.team2 : props.match.team1;
const otherTeamRoute = `/teams/${otherTeam}`;
const isMatchWon = props.teamName === props.match.matchWinner;
return (
<div className={isMatchWon ? 'MatchDetailCard won-card' : 'MatchDetailCard lost-card'}>
<div className="">
<span className="vs">vs</span><h1><Link to={otherTeamRoute}>{otherTeam}</Link></h1>
</div>
</div>
);
}
export {MatchDetailCard};
The problem that I'm facing is that the on-click of the link to /team/teamName route only the TeamPage component is not mounting instead it's just getting an update.
I want to have the call to componentDidMount hook to make the API call again in this scenario.
What's the problem with my logic?
If the same component is used as the child of multiple <Route>s at the same point in the component tree, React will see this as the same component instance and the component’s state will be preserved between route changes. If this isn’t desired, a unique key prop added to each route component will cause React to recreate the component instance when the route changes.
https://reactrouter.com/web/api/Route
You can add the teamName as a key prop on the component, which will tell React to unmount/mount the component when the key value changes.
<Router>
<Switch>
<Route
path="/teams/:teamName/matches"
render={({ match }) => {
return <MatchPage key={match.params.teamName} />;
}}
/>
<Route
path="/teams/:teamName"
render={({ match }) => {
return <TeamPage key={match.params.teamName} />;
}}
/>
</Switch>
</Router>

Redux state is not being passed to nested router component despite fetching the state directly in that component

I have a react-redux state that is a fetched array of 'products' objects from a backend database. I have a 'ProductList' component that receives the fetched objects and creates a list of products. I am directly calling my 'fetchProducts()' method within this 'ProductList' component. This 'ProductList' component is able to be refreshed and the fetched products will persist and reappear after each refresh.
However, I have another 'ViewProduct' component that is in a nested Router route in which I am also fetching the same 'products' array. The problem is in this component, on refresh, the 'products' state does not persist and returns undefined despite the fact that I am also directly calling 'fetchProducts()' inside the component.
I am passing the products array directly into the 'ProductView' component yet it does not persist on refresh the same way my 'ProductsList' component does.
Here is my 'AllProductsList' component that SUCCESSFULLY persists the products data on refresh. It is located in a route '/products/all':
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchProducts } from '../actions/productAction.js';
import ProductCard from '../components/ProductCard.js';
class AllProductsList extends Component {
componentDidMount() {
this.props.fetchProducts()
}
renderDiv = () => {
return this.props.products.map((product) =>
<ProductCard key={product.id} product={product} />
)
}
render() {
return(
<div>
{this.renderDiv()}
</div>
)
}
}
AllProductsList.propTypes = {
fetchProducts: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
}
const mapStateToProps = state => ({
products: state.products.items,
})
export default connect(mapStateToProps, { fetchProducts })(AllProductsList)
And here is the problem 'ProductView' component that does NOT persist the product data after refresh. This component is located on route /product/:id :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchProducts } from '../actions/productAction.js';
class ProductView extends Component {
constructor(props) {
super(props)
this.state = {
buttonMessage: 'Add to cart',
quantity: 0,
}
}
componentDidMount() {
this.props.fetchProducts()
}
quantityChangeReader = (e) => {
this.setState({ quantity: e.target.value })
}
render() {
return(
<div>
<ul>
<li>this.props.products[this.props.match.params.productId].name</li>
<li>this.props.products[this.props.match.params.productId].comments</li>
<li>this.props.products[this.props.match.params.productId].photos[1].url</li>
</ul>
</div>
)
}
}
ProductView.propTypes = {
fetchProducts: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
}
const mapStateToProps = state => ({
products: state.products.items,
})
export default connect(mapStateToProps, { fetchProducts })(ProductView)
Parent component of 'ProductView', which is routed to in the App.js:
import React from 'react';
import ProductView from '../components/ProductView.js';
import { Route } from 'react-router-dom';
const ProductViewContainer = ({ match, products }) => (
<div>
<Route path={`${match.url}/:productId`} render={routerProps => <ProductView {...routerProps} /> }/>
</div>
)
export default ProductViewContainer
In this second scenario, for component ProductView, on refresh, it will return the error message, 'Cannot read .name of undefined.' Yet in the first component ('AllProductsList') refreshing will successfully return the array again.
As requested, here is the router:
<Provider store={store}>
<Router>
<NavBar />
<Route exact path="/" component={Carousel} />
<Route exact path="/about" component={OurStory} />
<Route exact path="/products/necklaces" component={NecklacesList} />
<Route exact path="/products/bracelets" component={BraceletsList} />
<Route exact path="/products/earrings" component={EarringsList} />
<Route exact path="/products/all" component={AllProductsList} />
<Route exact path="/events" component={Events} />
<Route exact path="/cart" component={Cart} />
<Route path='/product' render={routerProps => <ProductViewContainer {...routerProps} />} />
<Credits />
</Router>
</Provider>
In the ProductView component, this.props.products will be undefined when the component renders for the first time. This will happen on page refresh or when you hit the product id route directly in the browser, either of which will reset your redux state.
To curb this, you'll need to add a check in your component to confirm if the products prop has been populated.
Like this...
render() {
const { products } = this.props;
if (products.length) {
// render the rest of your view
} else {
// products is still fetching...
}
}
React-redux does not persist data when you refresh page. If you want to persist data, store in localstorage.
When you refresh page, initially this.props.products will be null. Once data get fetch, this.props.products will have remote data, and component will get re-render. You can put if condition in ProductView component

Conditional Routing and Conditional Rendering in ReactJS and React router

I have a question, I tried looking online and couldn't find what I needed but that's probably because I couldn't word my question properly.
I have a React project I'm putting together and I want to have user profile pages for people. Here is what I have so far.
<Route path="/profile/:user" render={(routeProps) => (<Profile {...routeProps} {...this.state} /> )}/>
This is in my index.js, to set the route for my profile pages for different users and the following is what I have for my profile page
import React from 'react';
import ReactDOM from 'react-dom';
import { NavLink } from 'react-router-dom';
export class Profile extends React.Component{
constructor(props){
super(props);
this.state={
user: this.props.user,
fullname: this.props.fullname,
picture: this.props.picture
};
}
render(){
return(
<h1>{this.state.fullname}</h1>
);
}
}
Now the question I have is this. I want the profile page to only load and render the users full name if the URL matches the userID given by 'user' in the state
in my index.js I have hardcoded user and fullname values to test this, they are set like this
constructor(props){
super(props);
this.state = {
user:"AFC12345",
fullname:"Bob Ross"
};
I want to only have the Profile page rendered when I visit "http://localhost:8080/#/Profile/AFC12345" currently it renders the profile page for any "Profile/xxxx" I go to.
Another way would be to consider that Profilecontainer as a protected URL and use the same dynamic as the authentication flow.
import React from 'react';
import { Redirect } from 'react-router-dom';
const ProtectedProfile = ({ component: Component, ...rest }) => (
<Route {...rest} render={ props => (
props.user === props.match.params.user ? (
<Component {...props} /> ) : (
<Redirect to={{ pathname: '/404' }} /> )
)} />
);
Then in your App/index.js
<ProtectedProfile path="/profile/:user" component={Profile} />
i would do something like this in the render function
let viewToRender
if (this.state.user === this.props.match.params.user) {
viewToRender = <p>{this.state.fullname}</p>
}else{
viewToRender = <p>Ids don't match</p>
}
....
return (){ viewToRender }

How to redirect an user to a different url when history object of react-router is not accessible

How should I redirect an user to a different url when I cannot get an access to history props of react-router?
What I want to do is when an user clicks an log-out link on the navigation menu, the user get redirected to the root path '/'.
handleAuthentication(event) {
this.props.toggleAuthenticationStatus(() => {
// I want to redirect an user to the root path '/' in this callback function.
});
}
handleAuthentication method is called when an user clicks an login/logout link on the navigation menu.
toggleAuthenticationStatus(callback) {
this.setState((prevState, props) => {
return { isLoggedIn: !prevState.isLoggedIn }
},
callback()
);
}
Then, when handleAuthentication method in the NavigationMenu Component, it calls toggleAuthenticationStatus method in App Component that changes the state of Login/Logout and run callback function which is defined in the handleAuthentication method in the NavigationMenu Component.
Is it ok to run "window.location.href = '/'" directly?
Does it mess up the react-router history object???
Could anyone please how I should implement user redirect in a right way?
App Component
import React, { Component } from 'react';
import NavigationMenu from './NavigationMenu';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Secret from './Secret';
import Top from './Top';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false
};
this.toggleAuthenticationStatus = this.toggleAuthenticationStatus.bind(this);
}
toggleAuthenticationStatus(callback) {
this.setState((prevState, props) => {
return { isLoggedIn: !prevState.isLoggedIn }
},
callback()
);
}
render() {
return (
<BrowserRouter>
<div>
<NavigationMenu isLoggedIn={this.state.isLoggedIn} toggleAuthenticationStatus={this.toggleAuthenticationStatus} />
<Switch>
<Route path='/secret' render={(props) => <Secret isLoggedIn={this.state.isLoggedIn} {...props} />} />
<Route path='/' component={Top} />
</Switch>
</div>
</BrowserRouter>
)
}
}
NavigationMenu Component
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class NavigationMenu extends Component {
constructor(props) {
super(props);
this.handleAuthentication = this.handleAuthentication.bind(this);
}
handleAuthentication(event) {
this.props.toggleAuthenticationStatus(() => {
// I want to redirect an user to the root path '/' in this callback function.
});
}
render() {
return (
<ul>
<li><Link to='/'>Top</Link></li>
<li><Link to='/secret'>Secret</Link></li>
<li><Link to='/login' onClick={this.handleAuthentication}>
{this.props.isLoggedIn === true ? 'Logout' : 'Login'}
</Link></li>
</ul>
)
}
}
export default NavigationMenu;
I found the method 'withRouter' in react-router.
This seems the solution in my situation.
I'm going to try using it.
https://reacttraining.com/react-router/web/api/withRouter
You can get access to the history object’s properties and the closest
's match via the withRouter higher-order component. withRouter
will re-render its component every time the route changes with the
same props as render props: { match, location, history }.

React router Link not causing component to update within nested routes

This is driving me crazy. When I try to use React Router's Link within a nested route, the link updates in the browser but the view isn't changing. Yet if I refresh the page to the link, it does. Somehow, the component isn't updating when it should (or at least that's the goal).
Here's what my links look like (prev/next-item are really vars):
<Link to={'/portfolio/previous-item'}>
<button className="button button-xs">Previous</button>
</Link>
<Link to={'/portfolio/next-item'}>
<button className="button button-xs">Next</button>
</Link>
A hacky solution is to manaully call a forceUpate() like:
<Link onClick={this.forceUpdate} to={'/portfolio/next-item'}>
<button className="button button-xs">Next</button>
</Link>
That works, but causes a full page refresh, which I don't want and an error:
ReactComponent.js:85 Uncaught TypeError: Cannot read property 'enqueueForceUpdate' of undefined
I've searched high and low for an answer and the closest I could come is this: https://github.com/reactjs/react-router/issues/880. But it's old and I'm not using the pure render mixin.
Here are my relevant routes:
<Route component={App}>
<Route path='/' component={Home}>
<Route path="/index:hashRoute" component={Home} />
</Route>
<Route path="/portfolio" component={PortfolioDetail} >
<Route path="/portfolio/:slug" component={PortfolioItemDetail} />
</Route>
<Route path="*" component={NoMatch} />
</Route>
For whatever reason, calling Link is not causing the component to remount which needs to happen in order to fetch the content for the new view. It does call componentDidUpdate, and I'm sure I could check for a url slug change and then trigger my ajax call/view update there, but it seems like this shouldn't be needed.
EDIT (more of the relevant code):
PortfolioDetail.js
import React, {Component} from 'react';
import { browserHistory } from 'react-router'
import {connect} from 'react-redux';
import Loader from '../components/common/loader';
import PortfolioItemDetail from '../components/portfolio-detail/portfolioItemDetail';
import * as portfolioActions from '../actions/portfolio';
export default class PortfolioDetail extends Component {
static readyOnActions(dispatch, params) {
// this action fires when rendering on the server then again with each componentDidMount.
// but not firing with Link...
return Promise.all([
dispatch(portfolioActions.fetchPortfolioDetailIfNeeded(params.slug))
]);
}
componentDidMount() {
// react-router Link is not causing this event to fire
const {dispatch, params} = this.props;
PortfolioDetail.readyOnActions(dispatch, params);
}
componentWillUnmount() {
// react-router Link is not causing this event to fire
this.props.dispatch(portfolioActions.resetPortfolioDetail());
}
renderPortfolioItemDetail(browserHistory) {
const {DetailReadyState, item} = this.props.portfolio;
if (DetailReadyState === 'WORK_DETAIL_FETCHING') {
return <Loader />;
} else if (DetailReadyState === 'WORK_DETAIL_FETCHED') {
return <PortfolioItemDetail />; // used to have this as this.props.children when the route was nested
} else if (DetailReadyState === 'WORK_DETAIL_FETCH_FAILED') {
browserHistory.push('/not-found');
}
}
render() {
return (
<div id="interior-page">
{this.renderPortfolioItemDetail(browserHistory)}
</div>
);
}
}
function mapStateToProps(state) {
return {
portfolio: state.portfolio
};
}
function mapDispatchToProps(dispatch) {
return {
dispatch: dispatch
}
}
export default connect(mapStateToProps, mapDispatchToProps)(PortfolioDetail);
PortfolioItemDetail.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import Gallery from './gallery';
export default class PortfolioItemDetail extends React.Component {
makeGallery(gallery) {
if (gallery) {
return gallery
.split('|')
.map((image, i) => {
return <li key={i}><img src={'/images/portfolio/' + image} alt="" /></li>
})
}
}
render() {
const { item } = this.props.portfolio;
return (
<div className="portfolio-detail container-fluid">
<Gallery
makeGallery={this.makeGallery.bind(this)}
item={item}
/>
</div>
);
}
}
function mapStateToProps(state) {
return {
portfolio: state.portfolio
};
}
export default connect(mapStateToProps)(PortfolioItemDetail);
gallery.js
import React, { Component } from 'react';
import { Link } from 'react-router';
const Gallery = (props) => {
const {gallery, prev, next} = props.item;
const prevButton = prev ? <Link to={'/portfolio/' + prev}><button className="button button-xs">Previous</button></Link> : '';
const nextButton = next ? <Link to={'/portfolio/' + next}><button className="button button-xs">Next</button></Link> : '';
return (
<div>
<ul className="gallery">
{props.makeGallery(gallery)}
</ul>
<div className="next-prev-btns">
{prevButton}
{nextButton}
</div>
</div>
);
};
export default Gallery;
New routes, based on Anoop's suggestion:
<Route component={App}>
<Route path='/' component={Home}>
<Route path="/index:hashRoute" component={Home} />
</Route>
<Route path="/portfolio/:slug" component={PortfolioDetail} />
<Route path="*" component={NoMatch} />
</Route>
Could not get to the bottom of this, but I was able to achieve my goals with ComponentWillRecieveProps:
componentWillReceiveProps(nextProps){
if (nextProps.params.slug !== this.props.params.slug) {
const {dispatch, params} = nextProps;
PortfolioDetail.readyOnActions(dispatch, params, true);
}
}
In other words, for whatever reason when I use React Router Link to link to a page with the SAME PARENT COMPONENT, it doesn't fire componentWillUnMount/componentWillMount. So I'm having to manually trigger my actions. It does work as I expect whenever I link to Routes with a different parent component.
Maybe this is as designed, but it doesn't seem right and isn't intuitive. I've noticed that there are many similar questions on Stackoverflow about Link changing the url but not updating the page so I'm not the only one. If anyone has any insight on this I would still love to hear it!
It's good to share the components code also. However, I tried to recreate the same locally and is working fine for me. Below is the sample code,
import { Route, Link } from 'react-router';
import React from 'react';
import App from '../components/App';
const Home = ({ children }) => (
<div>
Hello There Team!!!
{children}
</div>
);
const PortfolioDetail = () => (
<div>
<Link to={'/portfolio/previous-item'}>
<button className="button button-xs">Previous</button>
</Link>
<Link to={'/portfolio/next-item'}>
<button className="button button-xs">Next</button>
</Link>
</div>
);
const PortfolioItemDetail = () => (
<div>PortfolioItemDetail</div>
);
const NoMatch = () => (
<div>404</div>
);
module.exports = (
<Route path="/" component={Home}>
<Route path='/' component={Home}>
<Route path="/index:hashRoute" component={Home} />
</Route>
<Route path="/portfolio" component={PortfolioDetail} />
<Route path="/portfolio/:slug" component={PortfolioItemDetail} />
<Route path="*" component={NoMatch} />
</Route>
);
componentWillReceiveProps is the answer to this one, but it's a little annoying. I wrote a BaseController "concept" which sets a state action on route changes EVEN though the route's component is the same. So imagine your routes look like this:
<Route path="test" name="test" component={TestController} />
<Route path="test/edit(/:id)" name="test" component={TestController} />
<Route path="test/anything" name="test" component={TestController} />
So then a BaseController would check the route update:
import React from "react";
/**
* conceptual experiment
* to adapt a controller/action sort of approach
*/
export default class BaseController extends React.Component {
/**
* setState function as a call back to be set from
* every inheriting instance
*
* #param setStateCallback
*/
init(setStateCallback) {
this.setStateCall = setStateCallback
this.setStateCall({action: this.getActionFromPath(this.props.location.pathname)})
}
componentWillReceiveProps(nextProps) {
if (nextProps.location.pathname != this.props.location.pathname) {
this.setStateCall({action: this.getActionFromPath(nextProps.location.pathname)})
}
}
getActionFromPath(path) {
let split = path.split('/')
if(split.length == 3 && split[2].length > 0) {
return split[2]
} else {
return 'index'
}
}
render() {
return null
}
}
You can then inherit from that one:
import React from "react";
import BaseController from './BaseController'
export default class TestController extends BaseController {
componentWillMount() {
/**
* convention is to call init to
* pass the setState function
*/
this.init(this.setState)
}
componentDidUpdate(){
/**
* state change due to route change
*/
console.log(this.state)
}
getContent(){
switch(this.state.action) {
case 'index':
return <span> Index action </span>
case 'anything':
return <span>Anything action route</span>
case 'edit':
return <span>Edit action route</span>
default:
return <span>404 I guess</span>
}
}
render() {
return (<div>
<h1>Test page</h1>
<p>
{this.getContent()}
</p>
</div>)
}
}
I got stuck on this also in React 16.
My solution was as follows:
componentWillMount() {
const { id } = this.props.match.params;
this.props.fetchCategory(id); // Fetch data and set state
}
componentWillReceiveProps(nextProps) {
const { id } = nextProps.match.params;
const { category } = nextProps;
if(!category) {
this.props.fetchCategory(id); // Fetch data and set state
}
}
I am using redux to manage state but the concept is the same I think.
Set the state as per normal on the WillMount method and when the WillReceiveProps is called you can check if the state has been updated if it hasn't you can recall the method that sets your state, this should re-render your component.
I am uncertain whether it fixes the original problem, but I had a similar issue which was resolved by passing in the function callback () => this.forceUpdate() instead of this.forceUpdate.
Since no one else is mentioning it, I see that you are using onClick={this.forceUpdate}, and would try onClick={() => this.forceUpdate()}.
Try to import BrowserRouter instead of Router
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom;
It worked for me after spending a couple of hours solving this issue.
I solved this by building '' custom component instead of '', and inside it I use in the method instead of :
import * as React from "react";
import {Navigate} from "react-router-dom";
import {useState} from "react";
export function ReactLink(props) {
const [navigate, setNavigate] = useState(<span/>);
return (
<div style={{cursor: "pointer"}}
onClick={() => setNavigate(<Navigate to={props.to}/>)}>
{navigate}
{props.children}
</div>
}

Resources