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.
Related
At the moment, I have the following routes in my App.js file:
<Switch>
<Route exact path="/new-job"
render={(props) => <NewJob jobName={jobName} setMenuSelection={handleMenuSelection} />}
/>
<Route exact path="/past-jobs"
render={(props) => <PastJobs setMenuSelection={handleMenuSelection} />}
/>
</Switch>
Now within my PastJobs component, I have the following button with onClick process:
<Button
onClick={() => {
setConfirmDialog({
isOpen: true,
title: `Copy Job ${item.id}?`,
onConfirm: () => { onCopy(item.job_info) }
})
}}
>
Copy
</Button>
that calls the following function:
const onCopy = (job_info) => {
setConfirmDialog({
...confirmDialog,
isOpen: false
})
history.push({
pathname: '/new-job',
state: { detail: job_info }
})
}
Within my <NewJob /> component, I have now setup the following as I thought I could access the state.detail but unfortunately it's null, i.e.:
import { useLocation } from 'react-router-dom';
function NewJob( { jobName, setMenuSelection } ) {
const { state } = useLocation();
if (typeof state !== 'undefined') {
const myVal = state.detail
console.log("myVal", myVal )
}
}
The issue that I am having and unsure how to approach is that within my onCopy function that is called from button onClick, how do I call the the <NewJob /> component whose path is exact path="/new-job" in App.js above and pass in the prop job_info ?
Direct calls to components actually does not exist. But what you are looking can be achieved in different ways.
Using state machine with event bus (redux, redux-saga)
Render props https://reactjs.org/docs/render-props.html
Bunch of callbacks drilled via props (HOC's)
Ref's https://reactjs.org/docs/refs-and-the-dom.html
I suggest to read more about them to actually understand if it matches your use case. Anyhow it is great experience to develop your skills also!
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?
I am working on a react application in which I have 3 component, AdminShopRoutes, AdminShop and Products.
The AdminShopRoutes Component:
const AdminShopRoutes = () => {
return (
<Router>
<Header>
<AdminShop exact path='/admin/shop' component={Dashboard} />
<AdminShop exact path='/admin/shop/customers' component={Customers} />
<AdminShop exact path='/admin/shop/products' component={Products} />
</Header>
</Router>)
}
The AdminShop Component
const AdminShop = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={(props) => (
true
? <Component {...props} />
: null
)} />
)
}
And finally the Product Component
const Products = (props) => {
useEffect(() => props.getProducts(), [])
const { products, loading } = props
return ( ... )
}
export default connect(mapStateToProps, { getProducts })(Products)
The links present in the other components beside these work i.e the url is changed but the page is blank as soon as the url changes for every route. If I then refresh the page, it renders fine but only on REFRESH. Also if I omit the route rendering the Products component all other routes work fine. Is there some other method of using hooks when working with react router because it is something to with Products component. Any help would be appreciated.
This is the warning I get when i render the product page
Arrow functions without {} will return the value of their one statement.
Arrow function with {} require an explicit return statement to return a value.
So the result of props.getProducts() is being returned from your effect.
However, useEffect() restricts the return value to only be a cleanup function for that effect. A non function return value is considered to be an error by React.
To fix this, just add {} to your arrow function so that it does not return a value:
useEffect(() => {
props.getProducts()
}, [])
do this
useEffect(() => {
props.getProducts()
}, [])
so that props.getProducts() doesn't get returned
Your error shows this has nothing to do with routing, but your useEffect is returning something when it shouldn't.
The error is pretty verbose and clear.
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.
In my App.js, I'm creating a Route as such:
<Route
exact path='/tools/ndalink'
render={(props) => (
<React.Fragment>
<Header />
<LinkPage {...props} brokerID={this.state.brokerID}></LinkPage>
</React.Fragment>
)}
/>
state.brokerID is initially "", but changed shortly after, therefore LinkPage receives this.state.brokerID as "".
How can I pass the changed state.brokerID (without using Redux)?
You need to use a lifecycle method to get the props to the component to wait for the props called componentDidUpdate.
That being said, you only have to use this if you plan to mutate the brokerId.
Since the process is async you'll have to wait for the props to be passed down. Until the you can show a loading text or progess bar.
class LinkPage extends React.Component {
state = {
builderId: ''
};
componentDidUpdate(prevProps) {
if(this.props.builderId !== prevProps.brokerId) {
this.setState({ builderId: this.props.brokerId });
}
}
render() {
return (
<div className="App">
<h1>{ this.state.builderId ? this.state.builderId : 'Loading' }</h1>
</div>
);
}
}
Or, a simple method would be to not use the lifecycle method at all, Change the following line in render and it should work:
<h1>{ this.props.builderId ? this.props.builderId : 'Loading' }</h1>
If you need to use this brokerId for an api call or something, you can use the setState callback. This would go something like this in the componentDidUpdate lifecycle method.
componentDidUpdate(prevProps) {
if(this.props.builderId !== prevProps.builderId) {
this.setState({ builderId: this.props.builderId }, () => {
//use this.state.brokerIdhere
});