React updating component when params change with old state - reactjs

i'm rendering some router links with params that contain the the same url but different id params. The router view updates but the state is always behind 1.
here's my Router setup:
<Router>
<div>
<h1>HEY THERE</h1>
<Link to={'/'}>Home</Link>
<Link to={'/detail/13721'}>Show Number 1 </Link>
<Link to={'/detail/1228'}>Show Number 2</Link>
</div>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/detail/:id" component={DetailPage} />
</Switch>
</Router>
Here's my detail page setup:
import React, { Component } from 'react'
class DetailPage extends React.Component {
constructor(props){
super(props);
this.state ={
showid: '30318',
showurl: 'http://localhost/podcast/podcastsbyid/?id=',
shows: []
}
}
render() {
return ( <div>
<h1>Detail Page</h1><p>{this.state.showid}</p>
{this.state.shows.map((show, i) => {
return <div key={i}>{show.title}</div>
})}
</div>
)
}
getShow(){
fetch(this.state.showurl + this.state.showid).then(res => res.json()).then(data => {
this.setState({shows: []})
this.setState({shows: this.state.shows.concat(data.items)})
})
}
componentWillReceiveProps(newProps){
if(this.state.showid == newProps.match.params.id){
console.log('they are the same')
}
else{
console.log('they are different')
this.setState({showid: newProps.match.params.id})
this.getShow()
}
}
}
export default DetailPage;
any help would be appreciated!!

The problem seems to be state update being asynchronous. Try updating the code as shown below:
componentWillReceiveProps(newProps){
if(this.state.showid == newProps.match.params.id){
console.log('they are the same')
}
else{
console.log('they are different')
this.setState({showid: newProps.match.params.id}, this.getShow)
// use it as callback ------------------------------^
}
}

Related

React) "this.props" while sharing the "state" using router

Somebody help me :(
I couldn't find "this.props.Epoint" on the result page.
It's just like "Epoint : " "IPoint : ". Empty, Empty.
I do have to receive "Epoint : 0", "IPoint : ", don't I?
Here is the code. Please save me.
<App.js>
class App extends Component {
state = {
EPoint: 0,
IPoint: 0,
};
upEPoint = async () => {
this.setState({
Epoint: this.state.EPoint ++
})
};
upIPoint = async () => {
this.setState({
Ipoint: this.state.IPoint ++
})
};
render() {
return (
<>
<Router>
<Route exact path="/" component={Home} />
<Route path="/question1" component={() => <Question1 EPoint={this.state.EPoint} IPoint={this.state.IPoint} upEPoint={this.upEPoint} upIPoint={this.upIPoint}/>} />
<Route path="/question2" component={() => <Question2 EPoint={this.state.EPoint} IPoint={this.state.IPoint} upEPoint={this.upEPoint} upIPoint={this.upIPoint}/>} />
<Route path="/question3" component={() => <Question3 EPoint={this.state.EPoint} IPoint={this.state.IPoint} upEPoint={this.upEPoint} upIPoint={this.upIPoint}/>} />
<Route path="/result" component={() => <Result EPoint={this.state.EPoint} IPoint={this.state.IPoint}/>} />
<Router/>
</>
export default App;
<Result.js>
class Result extends Component {
render() {
return (
<div>
<header>
<h1> Result </h1>
<h5> Epoint : {this.props.Epoint}</h5>
<h5> Ipoint : {this.props.Ipoint}</h5>
</header>
</div>)
}
}
export default Result;
I think the first issue here is that you are trying to access Epoint from props, but the variable in state that you are passing down in props is actually EPoint (notice the capital P there). Same goes for IPoint.
Your Result.js should look like this:
import React from "react";
class Result extends React.Component {
render() {
return (
<div>
<header>
<h1> Result </h1>
<h5> Epoint : {this.props.EPoint}</h5>
<h5> Ipoint : {this.props.IPoint}</h5>
</header>
</div>
);
}
}
export default Result;
As the other answers have also mentioned, you cannot set your state as you have.
I am not so good with class components, but I believe you must set it something like the following:
constructor(props) {
super(props);
this.state = { EPoint: 0, IPoint: 0 };
}
u cant use this.state inside setState just get prev state from arrow function then assign it to new object and return it into setState
upIPoint = async () => {
this.setState(prev => ({
Ipoint: prev.IPoint + 1
})
};

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

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

Why is this.props.history undefined despite having used withRouter?

I'm trying to do this.props.history.push... in my component, but even after making sure that I'm exporting it using withRouter I still get this error:
Uncaught TypeError: Cannot read property 'push' of undefined
I also made sure that the parent component that's using this is wrapped inside of a ProtectedRoute as well:
// my component:
import React from 'react';
import { withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
class UserIndexItem extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.play = this.play.bind(this);
}
handleClick(e) {
if (!e.target.classList.contains("triangle")) {
this.props.history.push(`/playlist/${this.props.playlist.id}`);
}
}
handleTrack(playlist) {
// still going forward one, then back one, and then it plays normally...
if (!playlist.payload.tracks) return;
let tracks = Object.values(playlist.payload.tracks);
let currentTrack = tracks[0];
let nextTrack = tracks[1];
this.props.receiveCurrentTrack(currentTrack);
this.props.receiveNextTrack(nextTrack);
this.props.receiveTitle(currentTrack.title);
this.props.receiveArtist(currentTrack.artist);
this.props.receiveAlbumId(currentTrack.album_id);
}
play() {
const { playlist } = this.props;
this.props.requestSinglePlaylist(this.props.playlist.id).then(playlist => this.handleTrack(playlist));
this.props.receivePlaylistId(playlist.id);
}
render() {
const { playlist } = this.props;
return (
<li>
<div className="playlist-image" onClick={ this.handleClick }>
<div className="play-button" onClick={ this.play }>
<div className="triangle right"></div>
<div className="circle"></div>
</div>
<div className="overlay"></div>
<img src={playlist.photo_url} alt="Playlist thumbnail" onClick={ this.handleClick }/>
</div>
<div className="playlist-name">
<Link to={`/playlist/${playlist.id}`}>{ playlist.title}</Link>
</div>
</li>
);
}
}
export default withRouter(UserIndexItem);
// my parent component:
import React from 'react';
import UserIndexItem from './user_index_item';
import { selectTracksFromPlaylist } from '../../reducers/selectors';
class UserIndex extends React.Component {
constructor(props) {
super(props);
}
render() {
const { user, playlists } = this.props;
return(
<div className="user-index-container">
<div className="header">
<h1>{ user.username }</h1>
<h2>Public Playlists</h2>
</div>
<div className="playlists">
<ul>
{ playlists.map(playlist =>
<UserIndexItem
key={ playlist.id }
playlist={ playlist }
requestSinglePlaylist={ this.props.requestSinglePlaylist }
receiveCurrentTrack={ this.props.receiveCurrentTrack }
receiveNextTrack = { this.props.receiveNextTrack }
receiveTitle={ this.props.receiveTitle }
receiveArtist={ this.props.receiveArtist }
receivePlaylistId={ this.props.receivePlaylistId }
receiveAlbumId={ this.props.receiveAlbumId }
/>)
}
</ul>
</div>
</div>
);
}
}
export default UserIndex;
// my route that's using the parent component:
<ProtectedRoute path="/users/:userId" component={UserIndex} />
// my ProtectedRoute implementation:
const Protected = ({ component: Component, path, loggedIn, exact }) => (
<Route path={ path } exact={ exact } render={ (props) => (
loggedIn ? (
<Component {...props} />
) : (
<Redirect to="/welcome" />
)
) }/>
);
You can try like this:
<ProtectedRoute path="/users/:userId" component={props => <UserIndex {...props} />} />
Please let me know if this is working.
Thanks.
I think that {...props} need to call inside UserIndexItem as well.
According to my understand inside the App.js you need to pass {...props} to child component otherwise it don't have parent properties
// this ProtectedRoute should change according to your requirement. I just put sample code
const ProtectedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
? <Component {...props} />
: <Redirect to="/Login"/>
)} />
)
<ProtectedRoute path="/users/:userId" component={UserIndex} />
// my parent component:
<UserIndexItem
key={ playlist.id }
playlist={ playlist }
requestSinglePlaylist={ this.props.requestSinglePlaylist }
receiveCurrentTrack={ this.props.receiveCurrentTrack }
receiveNextTrack = { this.props.receiveNextTrack }
receiveTitle={ this.props.receiveTitle }
receiveArtist={ this.props.receiveArtist }
receivePlaylistId={ this.props.receivePlaylistId }
receiveAlbumId={ this.props.receiveAlbumId }
{...this.props}
/>

Reactjs component does not call componentWillMount method when route is matched

I am trying to create an SPA using React.
I have an index.js and App.js, SidebarContentWrap.js, Sidebar.js, Content.js components.
index.js has BrowserRouter and calls App.js Component.
App.js fetches data from API in componentWillMount method and then renders a dynamic route <Route path={/playlist/:slug} component={SidebarContentWrap}/>
According to my understanding whenever route will match, componentWillMount in SidebarContentWrap will be called and I will fetch data in it and then render that data. But it does not happen.
Here is some of my code.
/*App.js*/
class App extends Component {
constructor(props){
super(props);
this.state = {
playLists: [],
dataRoute: `${Config.apiUrl}playlists?per_page=3`
}
}
componentWillMount(){
fetch(this.state.dataRoute)
.then(res => res.json())
.then(playlists => this.setState((prevState, props) => {
return { playLists : playlists.map( playlist => {
return { name: playlist.name, slug: playlist.slug}
}
)};
}));
}
render() {
return (
<div className="App">
<Header />
<switch>
{/*<Route path={`/playlist/:slug`} render={({match})=><SidebarContentWrap match={match} playLists={this.state.playLists}/>}/>*/}
<Route path={`/playlist/:slug`} component={SidebarContentWrap}/>
</switch>
<Footer />
</div>
);
}
}
export default App;
AND
/*SidebarContentWrap.js*/
class SidebarContentWrap extends Component {
constructor(props){
super(props);
}
componentWillMount(){
//FETCH DATA HERE EVERY TIME WHEN URL IS CHANGED
}
render() {
return (
<div className="sidebar-content-wrap">
<div className="wrap clearfix">
<main className="App-content">
{/*<Route path={`/playlist/:slug`} render={()=><Content/>}/>*/}
<Content />
</main>
<aside className="App-sidebar">
<div className="tabs">
{/*{this.props.playLists.map((playlist) =>*/}
{/*<NavLink key={playlist.slug} to={`/playlist/${playlist.slug}`}>{playlist.name}</NavLink>*/}
{/*)}*/}
<NavLink key="playlist-1" to="/playlist/playlist-1">Playlist 1</NavLink>
<NavLink key="playlist-2" to="/playlist/playlist-2">Playlist 2</NavLink>
<NavLink key="playlist-3" to="/playlist/playlist-3">Playlist 3</NavLink>
</div>
<div className="tabs-content">
{this.props.match.params.slug}
{/*<Route path={`/playlist/:slug`} render={()=><Sidebar/>}/>*/}
<Sidebar />
</div>
</aside>
</div>
</div>
);
}
}
export default SidebarContentWrap;
componentWillMount only get's called once when the component is first rendered. When your route changes, you aren't unmounting that component, so that's why componentWillMount never gets called again. What you want is to use componentWillReceiveProps instead. When you change the route, new router props will get passed to the component. So you should use componentWillReceiveProps to react to a url change.
You'll still want your fetch in componentWillMount for the very first time the component is rendered, but after that, the fetching should happen in componentWillReceiveProps.
class SidebarContentWrap extends Component {
constructor(props){
super(props);
}
componentWillMount(){
this.fetchData(this.props);
}
componentWillReceiveProps(nextProps){
this.fetchData(nextProps);
}
fetchData(props) {
//FETCH DATA HERE EVERY TIME WHEN URL IS CHANGED
}
render() {
return (
<div className="sidebar-content-wrap">
<div className="wrap clearfix">
<main className="App-content">
{/*<Route path={`/playlist/:slug`} render={()=><Content/>}/>*/}
<Content />
</main>
<aside className="App-sidebar">
<div className="tabs">
{/*{this.props.playLists.map((playlist) =>*/}
{/*<NavLink key={playlist.slug} to={`/playlist/${playlist.slug}`}>{playlist.name}</NavLink>*/}
{/*)}*/}
<NavLink key="playlist-1" to="/playlist/playlist-1">Playlist 1</NavLink>
<NavLink key="playlist-2" to="/playlist/playlist-2">Playlist 2</NavLink>
<NavLink key="playlist-3" to="/playlist/playlist-3">Playlist 3</NavLink>
</div>
<div className="tabs-content">
{this.props.match.params.slug}
{/*<Route path={`/playlist/:slug`} render={()=><Sidebar/>}/>*/}
<Sidebar />
</div>
</aside>
</div>
</div>
);
}
}
export default SidebarContentWrap;
If you change the route with a Link, the component doesn't get remounted, it re-renders. You need to add the fetching logic to componentWillReceiveProps
class SidebarContentWrap extends Component {
//...
componentWillReceiveProps(nextProps) {
//fetch
}
}

How can i dispatch something with react+redux+router from nav item without render anything?

My application has a map with a nav. The first two options open a modal window for configuration. For the third item, a want to execute a server process and update the map with the result, which is already rendered. How can I archive this?
Screen:
Routes:
class ModalSwitch extends React.Component {
constructor() {
super();
this.previousLocation = "/"
}
componentWillUpdate(nextProps) {
const { location } = this.props
if (
nextProps.history.action !== 'POP' &&
(!location.state || !location.state.modal)
) {
this.previousLocation = this.props.location
}
}
render() {
const { location } = this.props
const isModal = !!(
location.state &&
location.state.modal &&
this.previousLocation !== location
)
return (
<div>
<Switch location={isModal ? this.previousLocation : location}>
<Route path='/' component={Home} />
<Route path='/modal1/' component={Modal1} />
<Route path='/modal2/' component={Modal2} />
</Switch>
{isModal ? <Route path='/modal1/' component={Modal1} /> : null}
{isModal ? <Route path='/modal2/' component={Modal2} /> : null}
</div>
)
}
}
const Routes = () => (
<Router>
<Route component={ModalSwitch} />
</Router>
)
export default Routes
Menu (rendered by Home):
export default class Menu extends React.Component {
render() {
return (
<div>
<Link
key={0}
to={{
pathname: "/modal1",
state: { modal: true }
}}>
<p>Item 1</p>
</Link>
<Link
key={1}
to={{
pathname: "/modal2",
state: { modal: true }
}}>
<p>Item 2</p>
</Link>
</div>
);
}
}
Should I put a new Link to call a server process? If so, what I need to pass to pathname?
You should make your "link" be a dead link that calls a function
<a href="javascript:;" onClick={this.handleClick}>Process</a>
keep it an anchor tag so it will be styled the same as the React-Router's Link
then your handleClick function would look like this
handleClick = () => {
if (someValToMakeApiCall) {
this.props.myProcessAction(someData)
}
}

Resources