Hey,
I've got a PortfolioPage component (code below) calling a GET request to my api through the axios library inside the component useEffect.
After the request it sets the portfolio property using the setPortfolio hook.
Then, I pass the portfolio to the HoldingsChart component (Code for the component added). When I try using the portfolio prop passed inside the HoldingsChart component it shows me that the portfolio is an empty object.
I believe it is something related to the way setState is asynchronous but the other component (AssetChart) does get rendered correctly so I can't find the problem. Would really love some help with this.
PortfolioPage Component Code:
const PortfolioPage = ({match}) =>{
const user = useContext(UserContext)
const [portfolio, setPortfolio] = useState({})
const portfolioId = match.params.portfolioId
useEffect(() => {
// getPortfolio(user.token, portfolioId, setPortfolio)
const getPortfolio = async () => {
const domain = "http://127.0.0.1:8000/api"
await axios.get(domain + `/portfolios/${portfolioId}`,{
headers:{
'Authorization': `Token ${user.token}`
}
})
.then((res) => {
setPortfolio(res.data)
})
}
getPortfolio()
},[])
return (
<React.Fragment>
<AssetChart records={portfolio.records} />
<HoldingsChart portfolio={portfolio}/>
</React.Fragment>
)
}
HoldingsChart Component Code:
const HoldingChart = ({portfolio}) =>{
const holdings = portfolio.holdings
const totalValue = portfolio.total_value
const [data, setData] = useState([])
useEffect(() =>{
const holdingsPercentages = []
holdings.forEach(holding => holdingsPercentages.push({value: holding.total_value / totalValue}))
setData(holdingsPercentages)
},[])
const renderCustomizedLabel = ({
cx, cy, midAngle, innerRadius, outerRadius, percent, index,
}) => {
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
return (
<text x={x} y={y} fill="black" fontSize="10" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
{`${data[index].asset} ${(percent * 100).toFixed(0)}%`}
</text>
);
};
return (
<PieChart width={350} height={350}>
<Pie
data={data}
cx={175}
cy={100}
labelLine={false}
label={renderCustomizedLabel}
innerRadius={40}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{
data.map((entry, index) => <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />)
}
<Tooltip/>
</Pie>
</PieChart>
);
}
The Error:
TypeError: Cannot read property 'forEach' of undefined
(anonymous function)
C:/Users/David/Desktop/Long-Term/longterm/src/components/assets-comps/HoldingsChart.js:15
12 | const [data, setData] = useState([])
13 | useEffect(() =>{
14 | const holdingsPercentages = []
> 15 | holdings.forEach(holding => holdingsPercentages.push({value: holding.total_value / totalValue}))
| ^ 16 | setData(holdingsPercentages)
17 | },[])
18 |
Thanks
Just add a null check before you loop through holdings
if(holdings && holdings.length) {
holdings.forEach
}
or
holdings && holdings.length && holdings.forEach
or
holdings?.forEach
Reason:
Your holding chart component is rendered even before the api response is received. (It will be re-rendered again when api response is received , so adding a null check will solve the problem.
Even simpler and better solution
add a variable isResponseLoading to show a loader when api call is in progress & render your HoldingsChart component , only if api response is available.
{isResponseLoading && <Spinner />
{!isResponseLoading && portfolio?.length && <HoldingsChart portfolio={portfolio}/> }