Why the component doesn't update the props when changing the route param?
Trying to use setState inside componentWillReceiveProps but it doesn't even fire?
export default class HomePage extends React.PureComponent {
movies = require('../../movieList.json');
state = {
match: this.props.match,
id: null
}
componentWillReceiveProps(np) {
if(np.match.params.id !== this.props.match.params.id) {
console.log('UPDATING PROPS')
this.setState({ match: np.match })
}
}
render() {
const { match } = this.props;
const movie = this.movies[match.params.id || movieOrder[0]];
const nextMovie = getNextMovie(match.params.id || movieOrder[0]);
const prevMovie = getPrevMovie(match.params.id || movieOrder[0]);
return (
<>
<h1>{movie.title}</h1>
<Link to={nextMovie}>Next</Link>
<Link to={prevMovie}>Prev</Link>
</>
);
}
}
nextMovie and prevMovie get the id which should be set inside link. Unfortunatelly it sets only during 1st render. When clicked I can see the url change, but no updates are fired
Here is the component holding the switch
export default class App extends React.PureComponent {
state = {
isLoading: true,
}
componentDidMount() {
PreloaderService.preload('core').then(() => {
console.log('LOADED ALL');
console.log('PRELOADER SERVICE', PreloaderService);
this.setState({ isLoading: false });
console.log('Preloader assets', PreloaderService.getAsset('dog-video'))
});
}
render() {
const { isLoading } = this.state;
if(isLoading) {
return(
<div>
is loading
</div>
)
}
return (
<div>
{/* <Background /> */}
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path="/:id" component={props => <HomePage {...props} />} />
<Route component={NotFoundPage} />
</Switch>
<GlobalStyle />
</div>
);
}
}```
Seems to be working with my codesandbox recreation.
I had to improvise a bit with the missing code:
What's the value of movieOrder[0]? Is 0 a valid value for match.params.id?
How do getNextMovie and prevMovie look like?
Also, you're not using the Home component's state in your render logic, so I think, it should render the correct movie even without them.
There are several things off here. componentWillReceiveProps is considered unsafe and is deprecated. Try using componentDidUpdate instead. Also your class component needs a constructor. As it is now state is defined as a static variable.
constructor(props) {
super(props);
this.state = {
match: this.props.match,
id: null
}
}
componentDidUpdate(np) {
if(np.match.params.id !== this.props.match.params.id) {
console.log('UPDATING PROPS')
this.setState({ match: np.match })
}
}
Last, double check to make sure your logic is correct. Right now the Next and Prev buttons link to the same thing:
const nextMovie = getNextMovie(match.params.id || movieOrder[0]);
const prevMovie = getPrevMovie(match.params.id || movieOrder[0]);
<Link to={nextMovie}>Next</Link>
<Link to={prevMovie}>Prev</Link>
As an alternative, if you want to stick with componentWillReceiveProps you need to unbind the current movie, and rebind it with a new one. Here is a code example doing the exact same thing with users.
Related
I re-formatted the question with an example using the star wars API.
The problem I am having is trying to use state from PeoplePage and passing to the CounterPage while using react-router-dom. This is a small example of my real website. I am trying to narrow down the actual issue with this example. I hope this clarifies what I am working to achieve. Thanks!
import React from 'react';
import {BrowserRouter as Router,Switch,Route} from 'react-router-dom';
class ExampleApp extends React.Component{
constructor(){
super()
this.state = {
starWarsPeople:[],
stateNumber:0
}
}
numberHandler(number){
console.log('number',number)
this.setState({stateNumber: this.state.stateNumber + number})
}
componentDidMount(){
fetch('https://swapi.dev/api/people')
.then(response => response.json())
.then(data =>this.setState({starWarsPeople:data}))
}
render(){
const {starWarsPeople,stateNumber} = this.state
console.log(stateNumber);
return(
<Router>
<div>
<Switch>
/* Issue----->*/ <Route path ='/people' exact render = /*Render is where I am stuck */
{starWarsPeople.length !==0
?
<PeoplePage prop1 = {starWarsPeople} increment ={this.numberHandler.bind(this)}/>
:
<div>loading...</div>
}
/>
/* Issue----->*/ <Route path ='/counter' exact render =
/*render is where I am stuck, interval will not go to count prop when this is loaded*/
{starWarsPeople.length !==0
?
/*this example will render but not sure if its right and the interval will not work:
<Route path ='/counter' exact render = {
(props)=><CounterPage {...props} people ={starWarsPeople} key = {stateNumber}/>}/> */
<CounterPage people ={starWarsPeople} key = {stateNumber}/*I want this to re-render every time the inverval changes *//>
:
<div>loading...</div>
}
/>
</Switch>
</div>
</Router>
)
}
}
export default ExampleApp;
class PeoplePage extends React.Component {
constructor(props){
super(props)
this.state ={
number:0
}
}
componentDidMount(){
this.myInterval = setInterval(()=>{
this.setState({state:this.state+1})
this.props.increment(1);
},1000)
}
componentWillUnmount(){
clearInterval(this.myInterval);
}
render(){
return(
<div>
{this.props.prop1.results[0].name}
</div>
)
}
}
const CounterPage =({count})=>{
return(
<div>{count}</div>
)
}
I'm not entirely sure what your app code is supposed to be doing, but when I copy/pasted your code snippets into a codesandbox there were a few issues I had to address before the code would run.
Issues
Each Route's render prop wasn't returning a render function.
Each Route's path prop wasn't an absolute path.
PeoplePage's state update was incorrectly updating its state. Doesn't access the correct previous state, i.e. state isn't a number so this.state + 1 won't work.
CounterPage consumes and renders only a count prop, but isn't passed this prop.
Fixes
Routes
<Route
path="/people" // <-- fix path
exact
render={(routeProps) => // <-- use a render function to return UI
starWarsPeople.length !== 0 ? (
<PeoplePage
prop1={starWarsPeople}
increment={this.numberHandler.bind(this)}
{...routeProps} // <-- pass on route props
/>
) : (
<div>loading...</div>
)
}
/>
<Route
path="/counter" // <-- fix path
exact
render ={(routeProps) => // <-- use a render function to return UI
starWarsPeople.length !== 0 ? (
<CounterPage
people={starWarsPeople}
count={stateNumber} // <-- pass count prop
key={stateNumber}
{...routeProps} // <-- pass on route props
/>
) : (
<div>loading...</div>
)
}
/>
PeoplePage
class PeoplePage extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
};
}
componentDidMount() {
this.myInterval = setInterval(() => {
this.setState(({ number }) => ({ number: number + 1 }));
this.props.increment(1);
}, 1000);
}
componentWillUnmount() {
clearInterval(this.myInterval);
}
render() {
return <div>{this.props.prop1.results[0].name}</div>;
}
}
This is my render method in App.js:
render() {
console.log('APP Mounted', this.state.searchedBooks)
return (
<div className="app">
<Route exact path='/' render={()=> (
<ListBooks
books={this.state.books}
handleChangeCategory={(choice) => this.handleChangeCategory(choice)}
/>
)}/>
<Route path='/search' render={({ history }) => (
<SearchBook
handleChangeCategory={(choice) => this.handleChangeCategory(choice)}
handleSearchBook={(query) => this.handleSearchBook(query)}
searchedBooks={this.state.searchedBooks}
/>
)}/>
</div>
)}
I have searchedBook state that keeps updating when a new query arrives, and therefore also rerenders App.js. The log at the top of the render() method works as expected.
The problem is, that my child component SearchBook is not rerendering when App.js is rerendered and therefore doesn't receive the latest this.state.searchedBooks
In SearchBook.js I added the componentDidMount() method to reset the state:
class SearchBook extends Component {
state = {
query : '',
books : []
}
componentDidMount(){
console.log('Mounted')
this.setState(() => ({
books: this.props.searchedBooks
}))
}
updateQuery = (query) => {
this.setState(() => ({
query: query
}))
}
onChangeQuery = query => {
this.updateQuery(query)
this.props.handleSearchBook(query)
}
render(){
const { query, books } = this.state;
return()
}}
But this doesn't work as the whole SearchBook component doesn't get rerendered after my searchedBooks state is updated in App.js.
When I manually access a different URL (here the home route) and go back to the /search route, obviously the component gets rerendered and it works.
So how do I make the child of App.js rerender as well?
Why is the function of setting the side draw to false ignored when I can get the exact same function to work on click? I can even get the componentDidMount to console.log what is happening? I have been trying different ways to get it to work componentWillunmount etc but nothing other than onclick seems to work with it any thoughts?
export class App extends Component {
state = {
sideDrawerOpen: false
};
drawerToggleClickHandler = () => {
this.setState((prevState) => {
return { sideDrawerOpen: !prevState.sideDrawerOpen };
});
};
sidedrawerToggleClickHandler = () => {
this.setState({ sideDrawerOpen: false });
}
backdropClickHandler = () => {
this.setState({ sideDrawerOpen: false });
};
componentDidMount() {
this.setState({ sideDrawerOpen: false });
};
render() {
let backdrop;
if (this.state.sideDrawerOpen) {
backdrop = <Backdrop click={this.backdropClickHandler} />
}
return (
<div className="App_margin">
<Router>
<div className='App'>
<Nav drawerClickHandler={this.drawerToggleClickHandler} />
<SideDrawer sidedrawerClickHandler={this.sidedrawerToggleClickHandler} show={this.state.sideDrawerOpen} />
{ backdrop }
< Switch >
<Route path='/setup_page' component={setup_page} exact />
<Route path='/main_page' component={main_page} />
<Route path='/settings_page' component={settings_page} />
<Route component={Error} />
</Switch>
</div>
</Router>
</div>
);
}
}
Your state is initializing with state = { sideDrawerOpen: false }. When you run the componentDidMount() function you are trying to set this.state.sideDrawerOpen to false. The state is not changing, therefore there are no updates.
Since you are already initializing the sideDrawerOpen property to false, there is no need to set it to false again upon component mount.
Your componetsDidMount() is completely useless and your code is OK if you remove it.
I have a query.. I need to pass the value from one component to other component.. I want the value received (view console) after click event in the breedlist.js component to replace the base url value "akita" value in the breedimages.js
Refer to:
https://stackblitz.com/edit/react-kfwzgi
First and foremost, you will need to define additional props on both < <BreedImages /> and <BreedList /> components to handle the receiving of selected breeds, and selecting/clicking of breeds, respectively.
On your index.js,
const BASE_URL= "https://dog.ceo/api/breeds/list/all"
class App extends Component {
state={
breeds: [],
selectedBreed: undefined,
}
componentDidMount(){
fetch(BASE_URL)
.then(res => res.json())
.then(data => {
this.setState({
breeds: Object.keys(data.message)
})
})
}
handleSelect(selectedBreed) {
this.setState({ selectedBreed });
}
render() {
const { selectedBreed } = this.state;
return (
<Router>
<div className="App">
<Route path="/" exact strict render={
()=> {
return (<BreedList handleSelect={(val) => this.handleSelect(val)}/>)
}
}/>
<Route path="/images/" exact strict render={
()=> {
return (<BreedImages breed={selectedBreed}/>)
}
}/>
</div>
</Router>
);
}
}
Then, on your BreedList.js, you will need to modify the clickHandler method such that it is calling the handleSelect props method, to pass the selected dog breed back to the parent index.js component,
clickHandler(e) {
const val = e.currentTarget.value
this.props.handleSelect(val)
}
Then, on your BreedImages.js, you can listen to the prop updates on the componentDidUpdate lifecycle hook, and carry out the necessary operations from there.
I added an if statement to compare the prop values before doing any additional operations, as this will prevent any unnecessary re-rendering or requests.
componentDidUpdate(props, prevProps){
if (props.breed !== prevProps.breed) {
console.log(props);
// handle the rest
}
}
I have forked your demo and made the changes over here.
A button click shall filter my job-card array to only one category. E.g. button "Marketing" should filter to those jobs from array who have prop "jobstags: Marketing". I used a very similar procedure like for my input which filters jobs perfectly.
I can console log my event (the button click) with the according value ("Marketing"). But it still doesn't filter correctly...
In my app I did this:
export default class App extends Component {
state = {
jobs: jobs,
searchfield: '',
jobtags: ''
}
onSearchChange = event => {
this.setState({ searchfield: event.target.value })
}
onClickChange = event => {
console.log(event.target.value)
this.setState({ jobtags: event.target.value })
}
render() {
const filteredJobs = this.state.jobs.filter(job => {
return (
job.position
.toLowerCase()
.includes(this.state.searchfield.toLowerCase()) ||
job.company
.toLowerCase()
.includes(this.state.searchfield.toLowerCase()) ||
job.jobtags.toLowerCase().includes(this.state.jobtags.toLowerCase())
)
})
// this.save()
if (this.state.jobs.length === 0) {
return <Loading>Loading...</Loading>
} else {
return (
<Router>
<React.Fragment>
<Route
exact
path="/"
render={() => (
<Home
jobs={filteredJobs}
searchChange={this.onSearchChange}
clickChange={this.onClickChange}
/>
)}
/>
onClickChange is what should update the state of tags
In my Home component I then simply pass the value on to the Categories component:
<Categories clickChange={clickChange} />
Finally it arrives in my Categories component where I say:
export default class Categories extends Component {
render() {
const { clickChange } = this.props
return (
<Wrapper>
<button value="Marketing" onClick={clickChange}>
<img
alt="Button"
src={require('/Users/markus/Documents/q4-2018/jobs-app/src/img/computer.png')}
/>
Frontend
</button> ...
Any ideas? Thx!
maybe you have to bind the "this" of "onClickChange", for example in the constructor of your App class.
Example :
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
jobs: jobs,
searchfield: '',
jobtags: ''
};
this.onClickChange = this.onClickChange.bind(this);
and it will work I think
You will have to bind it. Add this line to your constructor:
this.onClickChange = this.onClickChange.bind(this);