1

When the main component loads, I send a GET request to my API to get all the objects that have certain value in a field called "ship_name", this means more "builds" might arrive, so there will be more to be rendered. After loading the main Component, I have created a second component in the same file, called "Builds". As props, I send to this component this.state.builds, an array of objects.

Better explanation:

  1. User inserts "ship_name" in a search bar
  2. API looks for all builds that have that "ship_name"
  3. All the builds are sent to the sub component
  4. Using the "gears" (Gear Names) API looks for the file name and sends it back
  5. ! React renders before the links arrive
<Builds builds={this.state.builds} />

The structure of "builds" is like this

[
    {
        "gears": [
            "Triple 410mm (10th Year Type Prototype)",
            "Triple 155mm (3rd Year Type)",
            "Twin 40mm Bofors STAAG Mk II",
            "High Performance Fire Control Radar",
            "Type 1 Armor Piercing Shell"
        ],
        "rarities": [
            "o",
            "o",
            "o",
            "o",
            "o"
        ],
        "_id": "5eac7b450e64096b082f4c43",
        "ship_name": "Gascogne",
        "pv": "e"
    },
    {
        "gears": [
            "Triple 410mm (10th Year Type Prototype)",
            "Triple 155mm (3rd Year Type)",
            "Twin 40mm Bofors STAAG Mk II",
            "High Performance Fire Control Radar",
            "Type 1 Armor Piercing Shell"
        ],
        "rarities": [
            "o",
            "o",
            "o",
            "o",
            "o"
        ],
        "_id": "5f131d6e2e3f16ef3de48e4f",
        "ship_name": "Gascogne",
        "pv": "e"
    }
]

Everything works till this point.

render() {
        return (
            this.props.builds.map((build) => (
                <div className="container" key={uuidv4()}>
                    <img src={"/img/gears/" + this.getGearData(build.gears[0])} className={build.rarities[0] + " gI g1"}></img><div className={"t t1"}>{build.gears[0]}</div>
                    <img src={"/img/gears/" + this.getGearData(build.gears[1])} className={build.rarities[1] + " gI g2"}></img><div className={"t t2"}>{build.gears[1]}</div>
                    <img src={"/img/gears/" + this.getGearData(build.gears[2])} className={build.rarities[2] + " gI g3"}></img><div className={"t t3"}>{build.gears[2]}</div>
                    <img src={"/img/gears/" + this.getGearData(build.gears[3])} className={build.rarities[3] + " gI g4"}></img><div className={"t t4"}>{build.gears[3]}</div>
                    <img src={"/img/gears/" + this.getGearData(build.gears[4])} className={build.rarities[4] + " gI g5"}></img><div className={"t t5"}>{build.gears[4]}</div>
                </div>
            ))

        )
    }

/*------------------------------*/

getGearData(gear_name) {
        let requestString = encodeURI('http://localhost:5000/gears/one?q=' + gear_name)
        let data;
        Axios.get(requestString)
            .then(res => {
                console.log(res.data[0].image_link)
                data = res.data[0].image_link
            })

        return data

        /* what */
    }

What happens here is that while I am rendering, at the same time I am trying to get the URL of the images. The GET methods works, in fact the console.log prints the actual name of the file

32200.png gear.component.js:76 
26600.png gear.component.js:76 
1260.png gear.component.js:76 
600.png gear.component.js:76 
34180.png gear.component.js:76 

but on the SRC the result is "/img/gears/undefined". One thing that I can't understand is why all the results are printed 2 times, but that's not much of a problem since in the end the things get rendered only once correctly.

I have tried to use many methods, state, Promises (even if I haven't really understood how those work), it's the first time I'm really stuck like this. The fact is that the main component uses the "ship_name" to get the file name of the "ship_icon" and the "faction_icon" and it gives no problem, probably because I use the state there.

Here's an idea of how it looks like right now

And here's an idea of how it should look like

I completed this personal project once using php and SQL, now I am trying to do it with React and MongoDB.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
Shalma
  • 21
  • 5
  • 2
    This piece of code has many problems. I think you should first learn about asynchronous JavaScript. Your `Axios.get()` call is async and you're returning `data` before even it's fetched. State management is the second thing. Your fetched data should be stored in component state. Thirdly, you need to call your APIs in `componentDidMount()` lifecycle hook or it's equivalent React hook. Clearing the concepts is more important here. Just a solution to this question won't help much, IMHO! – Akshit Mehra Jul 21 '20 at 19:48
  • 1
    Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Emile Bergeron Jul 21 '20 at 20:30
  • 1
    Then, specifically with React: [How to set state of response from axios in react](https://stackoverflow.com/q/41194866/1218980) – Emile Bergeron Jul 21 '20 at 20:31
  • 1
    Then, [looping inside JSX would help](https://stackoverflow.com/q/22876978/1218980). [An answer](https://stackoverflow.com/a/63022336/1218980) below also shows you how. – Emile Bergeron Jul 21 '20 at 20:32

2 Answers2

3

NOTE: you should be making the calls to fetch image URLs during the process that retrieves and/or sets the builds in the first place, this data structure is a sign of a poor design; now, that said:

  1. React is a reactive library. It only reacts to changes in data that it is monitoring.

  2. You can't mix asynchronous and synchronous functions and expect everything to wait.

when you call getGearData(gear_name) here is a rough rundown of what is happening in the order it is happening

  1. you call getGearData with variable gear_name
  2. a modifiable variable requestString is declared and set to encodeURI('http://localhost:5000/gears/one?q=' + gear_name) (side note, this should be const as it's not being modified
  3. a modifiable variable data is declared
  4. an asynchronous request is started to retrieve data from the server
  5. a reference to the current value of data is returned to the caller
  6. react renders
  7. your request finishes
  8. the local variable data is set within the function scope

you have to wait for async operations

there are a number of ways to do this. Here's an example using placeholder images

const placeholder = "https://link.to/placeholder-image.png"

class MyComponent extends Component {

  constructor(props) {
    super(props)
    this.state = { images: {} }
  }
  componentDidUpdate(prevProps) {
    if(prevProps.builds != this.props.builds) {
      this.fetchImages()
    }
  }

  fetchImages() {
    this.props.builds.forEach(build => {
      build.gears.forEach(gear => {
        Axios.get(encodeURI('http://localhost:5000/gears/one?q=' + gear_name))
          .then(res => {
            const link = res.data[0].image_link 
            console.log(link)
            this.setState(state => { images: { ...state.images, [gear]: link } })
          })
      })
    })
  }

  mapGears = (gears) =>
    gears.map(
      (gear, i) =>
        <img
          src={this.state.images[gear] || placeholder}
          className={`t t${i}`}
          key={`${gear}.${i}`}
        />
    )

  render() {
    return (
      this.props.builds.map((build) => (
        <div className="container" key={build._id}>
          {this.mapGears(build.gears)}
        </div>
      ))
    }
}
Sampson Crowley
  • 1,244
  • 1
  • 9
  • 22
  • Thanks a lot! I changed some small things and now it works wonderfully, here's how it looks [now](https://i.imgur.com/ZCAWVoU.png). Before I keep going I better study promises and asynchronous JS, I can clearly see what is my code and what is not. – Shalma Jul 21 '20 at 21:01
-1

That's because Axios.get is an asynchronous function, so you are returning variables before their data has been set.

I'd also say it's a poor design, it'd be a lot better to separate the concerns of fetching asynchronous data and rendering, eg.

async componentDidMount() {
    const containerToRender = (<></>);

    await Promise.all(
      this.props.builds.map(async (build) => {
        const container = <container ... key={build._id} />
        await Promise.all(build.gears.map(async (gear, i) => {
          container.children.push(<img src={"/img/gears/" + await this.getGearData(gear)} className={`${build.rarities[i]} gI g${i + 1}"} key={`img.${gear}.${i} />)
          container.children.push(<div className={`t t${i + 1}`} key={`div.${gear}.${i}>{gear}</div>)
        })
        containerToRender.children.push(container)
      })

    this.setState({ containerToRender });
}

getGearData(gear_name) {
  const requestString = encodeURI('http://localhost:5000/gears/one?q=' + gear_name)

  return Axios.get(requestString).then(res => res.data[0].image_link)
}

render() {
    return (this.state.containerToRender);
}
Sampson Crowley
  • 1,244
  • 1
  • 9
  • 22
k-wasilewski
  • 3,943
  • 4
  • 12
  • 29