Home > database >  React useState overwritten even with spread
React useState overwritten even with spread

Time:08-28

So I have a component where I have to make an API call to get some data that has IDs that I use for another async API call. My issue is I can't get the async API call to work correctly with updating the state via spread (...) so that the checks in the render can be made for displaying specific stages related to specific content.

FYI: Project is a Headless Drupal/React.

import WidgetButtonMenu from '../WidgetButtonMenu.jsx';
import { WidgetButtonType } from '../../Types/WidgetButtons.tsx';
import { getAllInitaitives, getInitiativeTaxonomyTerm } from '../../API/Initiatives.jsx';
import { useEffect } from 'react';
import { useState } from 'react';

import { stripHTML } from '../../Utilities/CommonCalls.jsx';

import '../../../CSS/Widgets/WidgetInitiativeOverview.css';

import iconAdd from '../../../Icons/Interaction/icon-add.svg';

function WidgetInitiativeOverview(props) {
    const [initiatives, setInitiatives] = useState([]);
    const [initiativesStages, setInitiativesStage] = useState([]);

    // Get all initiatives and data
    useEffect(() => {
        const stages = [];
        const asyncFn = async (initData) => {
            await Promise.all(initData.map((initiative, index) => {
                getInitiativeTaxonomyTerm(initiative.field_initiative_stage[0].target_id).then((data) => {
                    stages.push({
                        initiativeID: initiative.nid[0].value,
                        stageName: data.name[0].value
                    });
                });
            }));

            return stages;
        }

        // Call data
        getAllInitaitives().then((data) => {
            setInitiatives(data);
            asyncFn(data).then((returnStages) => {
                setInitiativesStage(returnStages);
            })
        });
    }, []);

    useEffect(() => {
        console.log('State of stages: ', initiativesStages);
    }, [initiativesStages]);

    return (
        <>
            <div className='widget-initiative-overview-container'>
                <WidgetButtonMenu type={ WidgetButtonType.DotsMenu } />
                { initiatives.map((initiative, index) => {
                    return (
                        <div className='initiative-container' key={ index }>
                            <div className='top-bar'>
                                <div className='initiative-stage'>
                                    { initiativesStages.map((stage, stageIndex) => {
                                        if (stage.initiativeID === initiative.nid[0].value) {
                                            return stage.stageName;
                                        }
                                    }) }
                                </div>
                                <button className='btn-add-contributors'><img src={ iconAdd } alt='Add icon.' /></button>
                            </div>
                            <div className='initiative-title'>{ initiative.title[0].value } - NID ({ initiative.nid[0].value })</div>
                            <div className='initiative-description'>{ stripHTML(initiative.field_initiative_description[0].processed) }</div>
                        </div>
                    );
                }) }
            </div>
        </>
    );
}

export default WidgetInitiativeOverview;

Here's a link for video visualization: https://vimeo.com/743753924. In the video you can see that on page refresh, there is not data within the state but if I modify the code (like putting in a space) and saving it, data populates for half a second and updates correctly within the component.

I've tried using spread to make sure that the state isn't mutated but I'm still learning the ins and outs of React.

The initiatives state works fine but then again that's just 1 API call and then setting the data. The initiativeStages state can use X amount of API calls depending on the amount of initiatives are returned during the first API call.

I don't think the API calls are necessary for this question but I can give reference to them if needed. Again, I think it's just the issue with updating the state.

CodePudding user response:

the function you pass to initData.map() does not return anything, so your await Promise.all() is waiting for an array of Promise.resolve(undefined) to resolve, which happens basically instantly, certainly long before your requests have finished and you had a chance to call stages.push({ ... });

That's why you setInitiativesStage([]) an empty array.

And what you do with const stages = []; and the stages.push() inside of the .then() is an antipattern, because it produces broken code like yours.

that's how I'd write that effect:

useEffect(() => {
  // makes the request for a single initiative and transforms the result.
  const getInitiative = initiative => getInitiativeTaxonomyTerm(
      initiative.field_initiative_stage[0].target_id
    ).then(data => ({
      initiativeID: initiative.nid[0].value,
      stageName: data.name[0].value
    }))

  // Call data
  getAllInitaitives()
    .then((initiatives) => {
      setInitiatives(initiatives);

Promise.all(allInitiatives.map(initiatives)).then(setInitiativesStage);
    });
}, []);

this code still has a flaw (imo.) it first updates setInitiatives, then starts to make the API calls for the initiaives themselves, before also updating setInitiativesStage. So there is a (short) period of time when these two states are out of sync. You might want to delay setInitiatives(initiatives); until the other API requests have finished.

getAllInitaitives()
  .then(async (initiatives) => {
    const initiativesStages = await Promise.all(initiatives.map(getInitiative));
    
    setInitiatives(initiatives);
    setInitiativesStage(initiativesStages)
  });
  • Related