Reputation: 411
First of, I'd like to let you know I'm just starting with React and the Context API.
Here is my situation : I'd like to fetch all beers from the PunkAPI at the startup of the app (displaying a loader while fetching) and put all those beers in a context to access it to sub-components.
My issue is that the API doesn't give the total of all beers pages so I have to check the length of the results and stop fetching when the length is equal to 0 then put the data in the state.
Then, once everything is loaded, I'd like to add a property (favorite) to all the beers and set it to false first, then, if the user is logged in, retrieve his favorites and set it to true for each beer in the context that is concerned by the list of favorites.
Here is what I came with for now :
import React from 'react'
import {jsonFetch} from 'easyfetch'
import _ from 'lodash'
import {UserAuthContext} from './userContext'
import {db} from '../firebase'
const defaultBeersContext = {
beers: null,
isFetchingBeers: true,
errorFetchingBeers: null,
}
export const BeersContext = React.createContext(defaultBeersContext)
export default class BeersProvider extends React.Component {
constructor(props) {
super(props)
this.state = defaultBeersContext
}
async componentDidMount() {
let allBeers = await this.fetchAllBeers(1, [])
allBeers.forEach(beer => (beer.favorite = false))
console.log(allBeers.length)
console.log(allBeers)
const {isUserSignedIn, user} = this.context
if (isUserSignedIn && user) {
db.collection('users')
.doc(user.uid)
.get()
.then(doc => {
const favoriteBeers = doc.data().favorites
favoriteBeers.forEach(elem => {
const index = allBeers.findIndex(beer => beer.id === elem)
allBeers[index].favorite = true
})
})
}
this.setState({beers: allBeers, isFetchingBeers: false})
}
fetchAllBeers(page, beers) {
jsonFetch(`https://api.punkapi.com/v2/beers?page=${page}&per_page=80`)
.get()
.then(data => {
if (data && data.length > 0) {
this.fetchAllBeers(page + 1, _.concat(beers, data))
} else {
return beers
}
})
.catch(error => this.setState({errorFetchingBeers: error, isFetchingBeers: false}))
}
render() {
const {children} = this.props
const {beers, isFetchingBeers, errorFetchingBeers} = this.state
return (
<BeersContext.Provider value={{beers, isFetchingBeers, errorFetchingBeers}}>{children}</BeersContext.Provider>
)
}
}
BeersProvider.contextType = UserAuthContext
P.S : The jsonFetch I'm using comes from here : easyfetch
But that is not working, saying :
Unhandled Rejection (TypeError): Cannot read property 'forEach' of null
21 | async componentDidMount() {
22 | this.fetchAllBeers(1, []).then(result => this.setState({beers: result}))
> 23 | this.state.beers.forEach(beer => (beer.favorite = false))
| ^ 24 | const {isUserSignedIn, user} = this.context
25 | if (isUserSignedIn && user) {
26 | db.collection('users')
Upvotes: 3
Views: 4763
Reputation: 411
thank you @Shawn Andrews for your help, I now understand more the process going behind Promises. I finally decided to move my merging of my favorites and my beers in a Children Container Component, letting the beersContext fetch all and the userContext fetching the favorites. Then, the container component takes the two and merges then passes the modified value to the rendering component !!
Here is the final BeersContext I have :
import React from 'react'
import {jsonFetch} from 'easyfetch'
import _ from 'lodash'
import {UserAuthContext} from './UserContext'
const defaultBeersContext = {
beers: null,
isFetchingBeers: true,
errorFetchingBeers: null,
}
export const BeersContext = React.createContext(defaultBeersContext)
export default class BeersProvider extends React.Component {
constructor(props) {
super(props)
this.state = defaultBeersContext
}
componentDidMount() {
this.fetchAllBeers(1, [])
}
async fetchAllBeers(page, beers) {
await jsonFetch(`https://api.punkapi.com/v2/beers?page=${page}&per_page=80`)
.get()
.then(data => {
if (data && data.length > 0) {
data.forEach(item => (item.favorite = false))
this.fetchAllBeers(page + 1, _.concat(beers, data))
} else {
this.setState({beers, isFetchingBeers: false})
}
})
.catch(error => this.setState({errorFetchingBeers: error, isFetchingBeers: false}))
}
render() {
const {children} = this.props
const {beers, isFetchingBeers, errorFetchingBeers} = this.state
return (
<BeersContext.Provider value={{beers, isFetchingBeers, errorFetchingBeers, fetchAllBeers: this.fetchAllBeers}}>
{children}
</BeersContext.Provider>
)
}
}
BeersProvider.contextType = UserAuthContext
Upvotes: 0
Reputation: 1442
In the error its stating you're trying to loop through the empty beers = null array in your defaultBeersContext.
I think your intention was to load the beers data from the API to fill the beer's state then forEach through that filled beer array.
You're getting the error because the following line is asynchronous and not completed before you try to access its returned beer data:
this.fetchAllBeers(1, []).then(result => this.setState({beers: result}))
Therefore on the next line
this.state.beers.forEach(...)
The state still contains the old default beers default data (beers: null) because your previous line hasn't finished fetching the beers data.
If you're using promises then you'd want to wait until you've fetched the new beers data before running your forEach, like so:
this.fetchAllBeers(1, [])
.then(result => {
this.setState({beers: result});
allBeers.forEach(beer => (beer.favorite = false));
...
})
You want to do this because your following lines depends on the completion of fetching the beers data.
Upvotes: 1