React router is not mounting the component - reactjs

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>

Related

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'params') [duplicate]

this is my code example but I do not know how to take the value and after use it
class View extends Component {
componentDidMount() {
var id = {match.params.id}
}
render() {
return(
<Router>
<div>
<Route path="/View/:id" component={Child}/>
</div>
</Router>
)
}
}
This might help you. Just create constructor i.e constructor(props) {} and inside it declare the id as a state variable to the class.
Pass the value of match.params.id to the id by using id: this.props.match.params.id.
Now u can access the state variable anywhere in your code and hope it solves your problem.
class View extends Component {
constructor(props){
super(props);
this.state = {
id : this.props.match.params.id
}
}
componentDidMount() {
var id = {this.state.id}
}
render() {
return(
<Router>
<div>
<Route path="/View/:id" component={Child}/>
</div>
</Router>
)
}
}
You can do it this way :
import { useParams } from 'react-router-dom';
function GetId() {
const { id } = useParams();
console.log(id);
return (
<div>
your expected id : {id}
</div>
);
}
{match.params.id} like variable.
this.id = this.props.match.params.id;
this.apartament = json.find((entry) => entry.id === this.id);
Try this:
<Route path=`/View/${id}` component={Child}/>
Look here:
https://github.com/reactjs/react-router-tutorial/tree/master/lessons/06-params
You component will be injected a prop params which you'll be able to get to like this:
<div>
<h2>{this.props.params.id}</h2>
</div>
I had the same issue, finally, this code worked.
It may be happening because you are following an older resource that is using an older version of react-router-dom version 5 whereas you are using version 6. In version 6 there were many breaking API changes. The Route components no longer use component or render props, the element prop that is passed a valid JSX literally replaced them. route props (history, location, and match) also no longer exist, the routed components must use the React hooks to access them now.
import { useParams } from 'react-router-dom';
const ProductDetails = () => {
const dispatch = useDispatch();
const { id } = useParams();
const {product,loading,error} = useSelector((state) => state.productDetails);
useEffect (()=>{
dispatch(getProductDetails(id))
},[dispatch,id])
return (
<Fragment>
<div className="ProductDetails">
<div>
<Crousel>
{product.images && product.images.map((item, i) => (
<img className='CrouselImage'
key={item.url}
src={item.url}
alt={`${i} side`} />
))}
</Crousel>
</div>
</div>
</Fragment>
)
}
Replace all your {match.params.id} with just id It should work fine.

How to call a state from a component to a page?

I am creating an example dApp which carries the "Header" component at the top of the page for every page. So, I have created a header component and I make people connect to their MetaMask wallet in Header.tsx, which they do successfully and I keep their wallet ID with currentAccount state.
Header.tsx:
const Header: FunctionComponent<{}> = (props) => {
const [currentAccount, setCurrentAccount] = useState("");
async function checkAccount() {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
setCurrentAccount(accounts[0]);
}
return (
<header>
<div className="col-xl-3 col-lg-3 col-md-3 col-sm-3">
<ul>
<li>{!connectHidden && <button className="buy connect-wallet" onClick={connectWallet}><b>Connect Wallet</b></button>}</li>
</ul>{currentAccount}
<ul>
<li>{!disconnectHidden && <button className="buy connect-wallet" onClick={disconnectWallet}><b>Disconnect Wallet</b></button>}</li>
</ul>
</div>
</header>
);
};
export default Header;
But at my homepage, there are no codes for anything about getting user's wallet ID, I don't want to rewrite the code here as it is not the right way. As a newbie in react, I couldn't make the codes I have tried work like trying to import a function or variables. How do I call the currentAccount state in my home page?
Home.tsx:
const HomePage: FunctionComponent<{}> = () => {
useEffect(() => {
onInit()
return () => { }
}, [])
async function onInit() {
}
async function onClickMint() {
alert("mint");
}
return (
<>
<div>xx
</div>
</>
);
};
export default HomePage;
Here is my app.tsx and as you can see, I am seeing all of the components at once. But I want to use the state I have got at Header component in my Home component.
App.tsx:
import Header from './components/Header';
const App: FunctionComponent<{}> = () => {
return (
<Router>
<Header />
<Route exact path="/" component={HomePage} />
<Route exact path="/wallet" component={Wallet} />
<Footer />
</Router>
);
};
export default App;
Quick answer:
simply create your state at the top level (App.tsx) and give currentAccount, setCurrentAccount as props for the other components
App.tsx:
import Header from './components/Header';
const App: FunctionComponent<{}> = () => {
const [currentAccount, setCurrentAccount] = useState("");
return (
<Router>
<Header />
<Route exact path="/">
<HomePage currentAccount={currentAccount} setCurrentAccount={setCurrentAccount}/>
</Route>
<Route exact path="/wallet">
<Wallet currentAccount={currentAccount} setCurrentAccount={setCurrentAccount}/>
</Route>
<Footer />
</Router>
);
};
export default App;
Longer answer:
You need to inform yourself about redux or simply the useContext hook
For instance with the useContext hook you can create a context that will contain your state and that you will be able to access in any child component without using props which can be redundant when you have multiple children and grandchildren ...
Here you can find the documentation about how to use the useContext Hook :
https://reactjs.org/docs/hooks-reference.html#usecontext

useParams hook returns undefined in react functional component

The app displays all photos <Photo> in a grid <PhotoGrid>, then once clicked, a function in <Photo> changes URL with history.push, and Router renders <Single> based on URL using useParams hook.
PhotoGrid -> Photo (changes URL onClick) -> Single based on URL (useParams).
I must have messed something up, becouse useParams returns undefined.
Thanks for all ideas in advanced.
App.js
class App extends Component {
render() {
return (
<>
<Switch>
<Route exact path="/" component={PhotoGrid}/>
<Route path="/view/:postId" component={Single}/>
</Switch>
</>
)
}
}
export default App;
Photogrid.js
export default function PhotoGrid() {
const posts = useSelector(selectPosts);
return (
<div>
hi
{/* {console.log(posts)} */}
{posts.map((post, i) => <Photo key={i} i={i} post={post} />)}
</div>
)
}
in Photo I change URL with history.push
const selectPost = () => {
(...)
history.push(`/view/${post.code}`);
};
Single.js
import { useParams } from "react-router-dom";
export default function Single() {
let { id } = useParams();
console.log("id:", id) //returns undefined
return (
<div className="single-photo">
the id is: {id} //renders nothing
</div>
)
}
When using useParams, you have to match the destructure let { postId } = useParams(); to your path "/view/:postId".
Working Single.js
import { useParams } from "react-router-dom";
export default function Single() {
const { postId } = useParams();
console.log("this.context:", postId )
return (
<div className="single-photo">
{/* render something based on postId */}
</div>
)
}
You should use the same destructure as mentioned in your Route path. In this case, you should have written :
let { postID } = useParams();
I will mention two more mistakes which someone could make and face the same problem:
You might use Router component in place of Route component.
You might forget to mention the parameter in the path attribute of the Route component, while you would have mentioned it in the Link to component.
Ensure the component where you call useParams() is really a child from <Route>
Beware of ReactDOM.createPortal
const App = () => {
return (
<>
<Switch>
<Route exact path="/" component={PhotoGrid}/>
<Route path="/view/:postId" component={Single}/>
</Switch>
<ComponentCreateWithPortal /> // Impossible to call it there
</>
)
}
You have to check API that you are using. Sometimes it's called not just id. That's why useParams() do not see it

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

React Router causes Redux container components re-render unneccessary

Here is my major code, App component is connected to Redux's store:
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostList} />
<Route path="/login" component={Login} />
<Route path="/topics" component={PostList} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
const mapStateToProps = (state, props) => {
return {
requestQuantity: getRequestQuantity(state)
};
};
export default connect(mapStateToProps)(App);
PostList component is also connected to Redux's store:
class PostList extends Component {
componentDidMount() {
this.props.fetchAllPosts();
}
render() {
const { posts} = this.props;
return (
// ...
);
}
//...
}
const mapStateToProps = (state, props) => {
return {
posts: getPostList(state),
};
};
const mapDispatchToProps = dispatch => {
return {
...bindActionCreators(postActions, dispatch),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
When this.props.fetchAllPosts() is called, the requestQuantity in the global state will change from 0 to 1 (request starts) then to 0 (request ends). So the App will re-render twice. However, every re-rendering of App also causes PostList to re-render, which is what I don't expect, since PostList only depends on posts in the global state and posts doesn't change in these twice re-rendering.
I check React Router's source code and find the Route's componentWillReceiveProps will always call the setState, which set a new match object:
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
)
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
)
//the official always set a new match object ignoring whether the nextProps change or not
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
})
}
It is the new match prop passed to the PostList causing the Redux's shallow comparison fails and re-rendering occurs. I hope React Router's team can do some easy logic before setState, such as using (===) comparing every prop in nextProps and this.props, if no change occurs, skip setState. Unfortunately,they think it is not a big deal and closed my issue.
Now my solution is creating a HOC :
// connectRoute.js
export default function connectRoute(WrappedComponent) {
return class extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.location !== this.props.location;
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
Then use connectRoute to wrap the containers used in Route:
const PostListWrapper = connectRoute(PostList);
const LoginWrapper = connectRoute(Login);
class App extends Component {
render() {
const { requestQuantity } = this.props;
return (
<div>
<Router>
<Switch>
<Route exact path="/" component={PostListWrapper} />
<Route path="/login" component={LoginWrapper} />
<Route path="/topics" component={PostListWrapper} />
</Switch>
</Router>
{requestQuantity > 0 && <Loading />}
</div>
);
}
}
Besides, when React Router is used with Mobx, this issue is also easy to meet.
Hope someone could offer better solutions. A long question. Thanks for your patience.

Resources