Home > Software design >  Make React JS await for a async func to complete before running
Make React JS await for a async func to complete before running

Time:10-14

I'm trying to make react not load until after an axios get requests finishes. I'm pretty rough on react all around, so sorry in advance.

I'm getting an array of objects const { dogBreedsTest } = useApplicationData()

And I need it to be the default value of one of my states const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest);

However, I'm getting an error that my value is coming up as null on the first iteration of my app starting. How can I ensure that my value has completed my request before my app tries to use it?

Here is how I am getting the data for useApplicationData()

const [dogBreedsTest, setDogBreeds] = useState(null);

  const getDogBreeds = async () => {
    try{
      const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
      if(data) {
        const newDogList = generateDogsArray(data['message'])
        const generatedDogs = selectedDogs(newDogList)
        setDogBreeds(generatedDogs)
        
      }
    } catch(err) {
      console.log(err);
    }
  }
  
  useEffect(() => {
    getDogBreeds()
  }, []);

  return {
    dogBreedsTest,
    setDogBreeds
  }

And I am importing into my app and using:

import useApplicationData from "./hooks/useApplicationData";

const { dogBreedsTest } = useApplicationData()
  const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest[0]);
  const [breedList1, updateBreedList1] = useState(dogBreedsTest[0])

function handleOnDragEnd(result) {
    if (!result.destination) return;

    const items = Array.from(dogBreeds);
    const [reorderedItem] = items.splice(result.source.index, 1);
    items.splice(result.destination.index, 0, reorderedItem);
    for (const [index, item] of items.entries()) {
      item['rank'] = index   1
    }
    updateDogBreeds(dogBreedsTest[0]);
    updateBreedList1(dogBreedsTest[0])
  }
return (
<div className="flex-container">
      <div className="App-header">
          <h1>Dog Breeds 1</h1>
          <DragDropContext onDragEnd={handleOnDragEnd}>
            <Droppable droppableId="characters">
              {(provided) => (
                <ul className="dogBreeds" {...provided.droppableProps} ref={provided.innerRef}>
                  {breedList1?.map(({id, name, rank}, index) => {
                    return (
                      <Draggable key={id} draggableId={id} index={index}>
                        {(provided) => (
                          <li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
                            <p>
                              #{rank}:  { name }
                            </p>
                          </li>
                        )}
                      </Draggable>
                    );
                  })}
                  {provided.placeholder}
                </ul>
              )}
            </Droppable>
            </DragDropContext>
        </div>
)

error: TypeError: Cannot read property 'map' of null (I am mapping the data later in the program)

CodePudding user response:

The problem is, that react first render and then run useEffect(), so if you don't want to render nothing before the axios, you need to tell to react, that the first render is null.

Where is your map function, to see the code? to show you it?.

I suppose that your data first is null. So you can use something like.

if(!data) return null

2nd Option:

    In your map try this:

   {breedList1 === null 
  ? null 
  : breedList1.map(({id, name, rank}, index) => (
     <Draggable 
      key={id} draggableId={id} index={index}>
                        {(provided) => (
                          <li ref={provided.innerRef}   {...provided.draggableProps} {...provided.dragHandleProps}>
                            <p>
                              #{rank}:  { name }
                            </p>
                          </li>
                        )}
                      </Draggable> ))}

You have null, because your axios is async and react try to render before any effect. So if you say to react that the list is null, react will render and load the data from the api in the second time.

CodePudding user response:

Option 1 use the optional chaining operator

dogBreedsTest?.map()

Option 2 check in the return if dogBreedsTest is an array

retrun (<>
    {Array.isArray(dogBreedsTest) && dogBreedsTest.map()}
</>)

Option 3 return early

if (!Array.isArray(dogBreedsTest)) return null

retrun (<>
    {dogBreedsTest.map()}
</>)

Option 4 set initial state

const [dogBreedsTest, setDogBreeds] = useState([]);

You could also add a loading state and add a loading spinner or something like that:

const [dogBreedsTest, setDogBreeds] = useState(null);
const [loading, setLoading] = useState(true)

  const getDogBreeds = async () => {
    setLoading(true)

    try{
      const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
      if(data) {
        const newDogList = generateDogsArray(data['message'])
        const generatedDogs = selectedDogs(newDogList)
        setDogBreeds(generatedDogs)
        
      }
    } catch(err) {
      console.log(err);
    }

    setLoading(false)
  }
  
  useEffect(() => {
    getDogBreeds()
  }, []);

  return {
    dogBreedsTest,
    loading,
    setDogBreeds
  }

Edit

Try to use a useEffect hook to update the states when dogBreedsTest got set.

const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest?.[0] ?? []);
const [breedList1, updateBreedList1] = useState(dogBreedsTest?.[0] ?? [])

useEffect(() => {
    updateDogBreeds(dogBreedsTest?.[0] ?? [])
    updateBreedList1(dogBreedsTest?.[0] ?? [])
}, [dogBreedsTest])

CodePudding user response:

const getDogBreeds = async () => {
  try {
    const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
      if(data) {
        const newDogList = generateDogsArray(data['message'])
        const generatedDogs = selectedDogs(newDogList)
        setDogBreeds(generatedDogs)
      }
  } catch(err) {
      console.log(err);
  }
}
  
useEffect(() => {
  getDogBreeds() // -> you are not awaiting this
}, []);

Do this instead

useEffect(() => {
  axios.get('https://dog.ceo/api/breeds/list/all')
    .then(res => {
      const newDogList = generateDogsArray(res.data['message']);
      const generatedDogs = selectedDogs(newDogList);
      setDogBreeds(generatedDogs);
    })
    .catch(err => console.log(err));
}, []);

I know this looks awful, but I don't think you should use async/await inside useEffect

Use this in your application

useEffect will update whenever dogBreedsTest is changed. In order to make it work, start with null values and update them to the correct initial values once your async operation is finished.

const { dogBreedsTest } = useApplicationData();
const [dogBreeds, updateDogBreeds] = useState(null);
const [breedList1, updateBreedList1] = useState(null);

useEffect(() => {
  updateDogBreeds(dogBreedsTest[0]);
  updateBreedList1(dogBreedsTest[0]);
}, [dogBreedsTest]);
  • Related