Home > Back-end >  Trouble to fill a dropdown menu using react
Trouble to fill a dropdown menu using react

Time:02-05

I have some problems with a project. I want to display some data, that I retreive from an API, in a dropdown menu. I used the .map() function and jsx to display everything, but nothing is working, the dropdown menu stay empty whereas a console.log(cities) show me that my array is filled.

function CitiesList(){
  var cities = citiesRequest();
  return (
    <form id="cityChoice">
      <label for="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city}</option>)}
      </select>
    </form>
  );
} 

If I change the way "cities" is fill for test, everything is working and the dropdown menu is filled :

function CitiesList(){
  var cities = ["Berlin", "Paris", "London"];
  return (
    <form id="cityChoice">
      <label for="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city}</option>)}
      </select>
    </form>
  );
} 

Here is entire my code if that can help :

function Header(props){
  return(
    <header>
      <h1 className="apiTitle">{props.name}</h1>
      <CitiesList/>       
    </header>
  );
}

function CitiesList(){
  var cities = citiesRequest();
  return (
    <form id="cityChoice">
      <label for="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city}</option>)}
      </select>
    </form>
  );
} 

ReactDOM.render(
  <Header name="Bike Mapper"/>,
  document.getElementById('root')
);
  
function citiesRequest(){
  var citiesArray = [];
  fetch("https://api.jcdecaux.com/vls/v3/contracts?apiKey=" APIKEY)
    .then((response) => response.json())
    .then(function(data){
    data.forEach(element => {
      if(element.country_code == 'FR'){
        citiesArray.push(element.name.toUpperCase());
      }
    }); 
  })
  return citiesArray;
}

Thanks

CodePudding user response:

Your city array is always empty because you are not waiting for the API to send the response back. To achieve this you can use Promises

function Header(props){
  return(
    <header>
      <h1 className="apiTitle">{props.name}</h1>
      <CitiesList/>       
    </header>
  );
}

async function CitiesList(){
  var cities = await citiesRequest();
  return (
    <form id="cityChoice">
      <label for="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city}</option>)}
      </select>
    </form>
  );
} 

ReactDOM.render(
  <Header name="Bike Mapper"/>,
  document.getElementById('root')
);
  
async function citiesRequest(){
  return new Promise((resolve,reject)=>{
   var citiesArray = [];
   fetch("https://api.jcdecaux.com/vls/v3/contracts?apiKey=" APIKEY)
     .then((response) => response.json())
     .then(function(data){
      data.forEach(element => {
        if(element.country_code == 'FR'){
          citiesArray.push(element.name.toUpperCase());
        }
       }); 
      resolve(citiesArray);
   })
   
  })
}

Using Hooks

import React, { useState, useEffect } from 'react';

function Header(props){
  const [cities, setCities] = useState([])
  useEffect(()=>{
     citiesRequest().then(data=>{
       setCities(data)
     });
   },[])
  return(
    <header>
      <h1 className="apiTitle">{props.name}</h1>
      <form id="cityChoice">
      <label for="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city} 
      </option>)}
      </select>
    </form>     
    </header>
  );
}   
ReactDOM.render(
  <Header name="Bike Mapper"/>,
  document.getElementById('root')
);
  
async function citiesRequest(){
  return new Promise((resolve,reject)=>{
   var citiesArray = [];
   fetch("https://api.jcdecaux.com/vls/v3/contracts?apiKey=" APIKEY)
     .then((response) => response.json())
     .then(function(data){
      data.forEach(element => {
        if(element.country_code == 'FR'){
          citiesArray.push(element.name.toUpperCase());
        }
       }); 
      resolve(citiesArray);
   })
   
  })
}

CodePudding user response:

using useState useEffect to have the page re-render on cities state change

function CitiesList(){
  const [cities, setCities] = React.useState([])

 React.useEffect(()=>{
  if(!cities.length){
   citiesRequest().then(result=>setCities(result)).catch(console.log)
  }
 },[cities])

  return (
    <form id="cityChoice">
      <label for="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city}</option>)}
      </select>
    </form>
  );
} 

function citiesRequest(){
 return new Promise((resolve, reject)=>{
  var citiesArray = [];
  fetch("https://api.jcdecaux.com/vls/v3/contracts?apiKey=" APIKEY)
    .then((response) => response.json())
    .then(function(data){
    data.forEach(element => {
      if(element.country_code == 'FR'){
        citiesArray.push(element.name.toUpperCase());
      }
    }); 
  }).catch(err=>reject(err))
  resolve(citiesArray);
 })
}

CodePudding user response:

I think the problem is that by the time that data is returned from your citiesRequest function, the component rendered already and has no indication that it should re-render.

You can try to:

  • modify citiesRequest function to return the data directly
  • call it within useEffect (with empty dependency array - to call it just once when the component mounts) in your component
  • await the response and then set it to component state (setCities).

When state changes, React will know it should re-render.

function CitiesList(){
  const [cities, setCities] = useState([]);

  useEffect(() => {
    const getData = async () => {
      const data = await citiesRequest();
      setCities(data);
    }
    
    getData();
  }, [])

  return (
    <form id="cityChoice">
      <label for="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city}</option>)}
      </select>
    </form>
  );
} 
  
function citiesRequest(){
  return fetch("https://api.jcdecaux.com/vls/v3/contracts?apiKey=" APIKEY)
    .then((response) => response.json())
    .then(function(data){ 
      return data
    })
}

CodePudding user response:

You need to get your data, then re-render the page, otherwise your page will render with nothing, then you'll get the data, and nothing will happen with it. This is where useEffect comes in. Basically after a render, useEffect will run (unless you tell it not to). The status-quo is to go get that information, then save it within the component's state (useState), and voila, you'll have the data.

In my example I use async await instead of .then() as it's a newer format; however, both are completely acceptable. It also matches how newer react documentation does it (see the small demo)

//Your CitiesList component
import React, {useState, useEffect} from 'react'

const CitiesList = () => {

  // Save as empty array so .map() doesn't fail.
  // You could also check if cities exists (else render null) in the return method
  const [cities, setCities] = useState([]) 

  useEffect(() => {
    const getCityData = async() => {
      let citiesArray = []
      const response = await fetch("https://api.jcdecaux.com/vls/v3/contracts?apiKey=" APIKEY)
      const cities = await response.json()
      cities.forEach(element => {
        if(element.country_code == 'FR'){
          citiesArray.push(element.name.toUpperCase());
        }
      }); 
      setCities(citiesArray)
    }
    getCityData()
  }, []) // This [] here basically says "only run this once after the initial render and then never again." 

  return (
    <form id="cityChoice">
      <label htmlFor="citySelected">Ville : </label>
      <select name="citiesList" id="citySelected">
        {cities.map((city) => <option key={city} value={city}>{city}</option>)}
      </select>
    </form>
  );
}

export default CitiesList

then in your Header,

//Your Header component
import React from 'react'
import CitiesList from "./CitiesList";

const Header = (props) => {

  return(
    <header>
      <h1 className="apiTitle">{props.name}</h1>
      <CitiesList/>
    </header>
  );
}

export default Header

I've also converted the single file into individual component files. You don't have to do this and can keep it all in one file, but as you build out your application, it makes organizing files much easier.

  •  Tags:  
  • Related