Search code examples
reactjsaxiosuse-effectsetstate

React - State set inside useEffect axios request is empty


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


Solution

  • 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}/> }