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?
Related
I am developing an android app using react-native and i have a but of a struggle of figuring out why my component re-renders on each page navigation.
In my case i have created a wrapper component called Container.android.js
const Container = props => {
useEffect(() => {
// fetching some data from async storage
},[])
// Using my hook here
const {code, error} = usePdaScan({
onEvent: (code) => {
// setting state via useState
},
onError: (error) => {
Alert.alert('Error', error);
},
trigger: 'always'
});
return <View>
{props.children}
</View>
}
Then i declare routes with Stack.Screen where each stack uses a component wrapped with the Container component.
<Stack.Screen name="Overview" component={Overview} />
And my Overview compnent is this
const Overview = props => {
return <Container>
<Text>Overview page</Text>
</Container>
}
My problem is that inside the Container component, there is a hook called usePdaScan. Each time i navigate to another page and the hook gets called, the Container component re-renders twice... I cant get a lead on this... Halp!
UPDATE: My homepage is a class component where it seems to work ok (render only once)
class Home extends Component {
state = {
productBarcodes: null
}
componentDidMount(){
// get data fetches data from async storage
getData('#productBarcodes').then(res => {
this.setState({
...this.state,
productBarcodes: JSON.parse(res)
})
})
}
render() {
const { productBarcodes } = this.state;
return (
<Container {...this.props}>
<View style={{marginBottom: 20, flex:1}}>
{productBarcodes.length} Products
</View>
</Container>
)
}
}
The usePdaScan hook in the Container component gets called once on my Homepage but twice on every other page like Overview etc... Both Homepage and Overview are wrapped with my Container component
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>;
}
}
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.
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.
Here is the parent component :
state = {
books: undefined
}
componentDidMount() {
BooksAPI.getAll().then(books => {
this.setState({books},this.filterBooks);
});
}
filterBooks() {
this.currentlyReading = this.state.books.filter((book) => book.shelf === 'currentlyReading');
this.read = this.state.filter((book) => book.shelf === 'read');
this.wantToRead = this.state.filter((book) => book.shelf === 'wantToRead');
}
render() {
return (
<div className="app">
<Route path="/search" component={SearchBook} />
<Route exact
path="/"
render={() => <BookScreen
currentlyReading={this.currentlyReading}
read={this.read}
wantToRead={this.wantToRead}
/>} />
</div>
)
}
I expect that the props will change for BookScreen component after filterBooks is called and the component should rerender, but it doesn't. What am I doing wrong?
In my opinion, the best way for now, which still follows React's way is: after having books data, call 1 function only, and this function will process everything and update state after finishing (we pass the books object as parameter), like this:
componentDidMount() {
BooksAPI.getAll().then(books => {
this.filterBooks({books});
});
}
filterBooks = (books) => {
this.currentlyReading = books.filter((book) => book.shelf === 'currentlyReading');
this.read = books.filter((book) => book.shelf === 'read');
this.wantToRead = books.filter((book) => book.shelf === 'wantToRead');
this.setState({ books: books });
}
If you have any error, feel free to post here then we can get through together!
========
Added explanation on why the author's original code doesn't work:
Based on my little experience with React and JS:
When a new state is set, it may take time (maybe 100-300ms, that is why you execute the original this.filterBooks using the syntax this.setState({books},this.filterBooks)
=> This seems to be right, which means after a new state of books is already set, you can access it the filterBooks function.)
HOWEVER: after new state of books is set, the page will be re-rendered, and filterBooks will be executed (perhaps at the same time => not sure which one ends first, so let's say for example, this.currentlyReading is still undefined in render() if render() happens first, before the result of filterBooks is completely set!
In other words, React is Javascript, and Javascript's asynchronization is troublesome!
You can try to do this, Just update the books state like this:
componentDidMount() {
BooksAPI.getAll().then(books => this.setState({books}));
}
This will cause a re-render. But since you're not using the state directly to populate your child component, we need to call the filterBooks() inside the render() method.
render() {
this.filterBooks()
return (
<div className="app">
<Route path="/search" component={SearchBook} />
<Route exact
path="/"
render={() => <BookScreen
currentlyReading={this.currentlyReading}
read={this.read}
wantToRead={this.wantToRead}
/>} />
</div>
)
}
The call to that method will update the data that you pass as props to your child component.