1

So I'm taking a course in web programming and in it we've gotten this assignment to design some simple front end for ordering salads, to get all the components etc. it was previously stored in a .js file in the following fashion

let inventory = {
    Sallad: {price: 10, foundation: true, vegan: true},
    Pasta: {price: 10, foundation: true, gluten: true},
    'Salad + Pasta': {price: 10, foundation: true, gluten: true},
    'Salad + Matvete': {price: 10, foundation: true, vegan: true, gluten: true},

    'Kycklingfilé': {price: 10, protein: true},
    'Rökt kalkonfilé': {price: 10, protein: true},

    'Böngroddar': {price: 5, extra: true, vegan: true},
    'Chèvreost': {price: 15, extra: true, lactose: true},

    Honungsdijon: {price: 5, dressing: true, vegan: true},
    Kimchimayo: {price: 5, dressing: true},

    .
    .
    .

};

export default inventory;

This is then imported into my App.js that was created when creating the react project and sent as a prop to another component that took care of the composing of a salad that was eventually sent back to a function also sent with as a prop.

So what we're supposed to do now is to get this inventory from a local rest(?) server instead. So if I go to

http://localhost:8080/proteins

it will open a page that just displays an array with all the different choices of proteins

["Kycklingfilé","Rökt kalkonfilé","Norsk fjordlax","Handskalade räkor från Smögen","Pulled beef från Sverige","Marinerad bönmix"]

And then going to

http://localhost:8080/proteins/Kycklingfilé

Will give you another page with the properties of that ingredient

{"price":10,"protein":true}

And my attempt at recreating that inventory object with all the ingredients as properties inside state is this

class App extends Component {

constructor(props) {
    super(props);
    this.state = {
        salads: [],

        inventory: {

        }
    };
}

componentDidMount() {
    const base = "http://localhost:8080/";
    const pURL =  base + "proteins/";
    const fURL =  base + "foundations/";
    const eURL =  base + "extras/";
    const dURL =  base + "dressings/";

    fetch(fURL).then(response => response.json()).then(data => {
        data.forEach(e => {
            fetch(fURL + e).then(response => response.json()).then(data => {
                Object.assign(this.state.inventory, {e : data})
            })
        })
    });

    fetch(pURL).then(response => response.json()).then(data => this.setState({data}));
    fetch(eURL).then(response => response.json()).then(data => this.setState({data}));
    fetch(dURL).then(response => response.json()).then(data => this.setState({data}));
}

I've been using

{JSON.stringify(this.state)}

to try and look at whats going on and with this code it comes out as this

{"salads":[],"inventory":{},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}

So the fetch works fine for getting all the ingredients of a certain type, I guess it's only the dressings since it overwrites data each time on those last three fetches. But the problem is that inventory is completely empty.

If I instead write it like this

fetch(fURL).then(response => response.json()).then(data => {
        data.forEach(e => {
            Object.assign(this.state.inventory, {e: fetch(fURL + e).then(response => response.json().then())})
        })
    });

The output becomes

{"salads":[],"inventory":{"e":{}},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}

So it adds the 'e' object, which is another problem since I want it to be the value of the current element, but it's completely empty, and I dont know how to get the data from that seconds fetch when I write it like that. So that's why it now looks like it does in the first code snippet, where it doesn't even get an empty 'e' inside inventory.

Finally, if I write it like that second example but just e: e like this

fetch(fURL).then(response => response.json()).then(data => {
        data.forEach(e => {
            Object.assign(this.state.inventory, {e: e})
        })
    });

The output becomes

{"salads":[],"inventory":{"e":"Salad + Quinoa"},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}

So it seems like everything is working up until the .forEach on the array of strings that represents a certain type of ingredient since it manages to put that into 'e' inside inventory with one of the array elements as it's value. It's only the last one in the list though but I guess that stems from the problem that it just makes the object 'e' and not the value of the current element and overwrites it for every item.

Sorry if all the rambling made the problem unclear, but what I'm trying to achieve is inventory {} inside state that looks like it did when it was in a seperate file, so that when we create the component we can send this.state.inventory instead of the imported inventory as prop. And to create that using what we can fetch from the different pages.

2 Answers2

1

When you write

{e : data}

you create a new Object with a single entry. That sets the value of the key 'e' as the current value of the variable 'data'. A variable named 'e' is not involved:

const e = 'hello';
console.log(e); // "hello"
console.log({ asd: e }); // { asd: "hello" }
console.log({ e: "asd" }); // { e: "asd" }
console.log({ e: asd }); // ReferenceError: asd is not defined

What you are trying to do is using the value of the variable e as the key that you want to set. In javascript this is done using [ and ] like so:

const e = 'hello';
console.log({ [e]: "world" }); // { hello: "world" }

// this is necessery whenever you want a key that is not a simple word
console.log({ ["key with spaces"]: "world" }); // { "key with spaces": "world" }
console.log({ [e + e]: "world" }); // { hellohello: "world" }

EDIT: there is another issue with your code above that you might encounter sooner or later:

In React you should never ever modify this.state directly. Always go through this.setState()!

https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly

In your case this is a bit more difficult, since you are making multiple requests which each affect the same key in your state (inventory). Because you cannot know in what order the requests arrive, and whether React will actually do the setState each time new data comes, or do them all at the same time, you cannot simply use this.setState({ inventory: newInventory }). Instead you should use the function version as described here. Unfortunately this can be a bit complex to grasp in the beginning :(

in your case I would solve it like this:

fetch(fURL).then(response => response.json()).then(data => {
    data.forEach(e => {
        fetch(fURL + e)
          .then(response => response.json())
          .then(data => this.setState((prevState) => ({
            inventory: Object.assign({}, prevState.inventory, {[e]: data}),
          })));
          })
    })
});

A couple of things to note here:

  1. note the ({ in (prevState) => ({ ... }): this is an arrow function that returns an object
  2. we are passing a function to this.setState (see the link above for details). This function receives the current state as an argument (prevState) and should return the new State. (although it can omit keys of the old state that remain unchanged). This is better than directly passing the new state to this.setState because when multiple setState happen at the same time, React can apply the functions you pass in the right order so that all changes happen, but if you passed objects it has to decide on one of them to 'win' so changes can get lost.
  3. In Object.assign({}, prevState.inventory, {[e]: data}), instead of modifying prevState.inventory we create a new object that contains the updated inventory. You should never modify the old state, even in this.setState.

Hope this helps :)

s-ol
  • 1,674
  • 17
  • 28
  • Perfect, thank you! Now it comes out with an object for every ingredient the way it's supposed to be, now I just need to figure out how to get the properties of each there as well. Thinking that maybe the problem is that the properties for an ingredient is also fetched as a string, and wont count as a object when put as source of the Object.assign – PatchingMatching Feb 18 '19 at 14:51
  • @eliRud I updated my answer with some more important info regarding your use of `this.state`. What do you mean about the properties? If each ingredient is represented as an object now, where are the properties if not in the object? – s-ol Feb 18 '19 at 15:13
  • So now when I'm using [e] it correctly creates that property with the name of the ingredient inside inventory. Thing is that, that property is supposed to be an object itself with the properrties of that ingredient. Which is supposed to be the thing that is fetched in that second fetch inside the forEach. In the end it's supposed to look like it did when inventory was in a seperate file and not in state. – PatchingMatching Feb 18 '19 at 15:22
  • So we first fetch the ingredient names from /category, we then fetch that ingredients properties from /category/ingredientname. Which is supposed to give us the price and category of the ingredient. And now it works correctly when assigning all the object, now I just need to get the right properties in there as well. – PatchingMatching Feb 18 '19 at 15:27
0

So with @sol's advice to use [e] to create the objects for each ingredient, this code

    fetch(fURL).then(response => response.json()).then(data => {
        data.forEach(e => {
            fetch(fURL + [e]).then(response => response.json()).then(data => {
                Object.assign(this.state.inventory, {[e] : data})
            })
        })
    });

now works. I think why it didn't look successful with my "troubleshooting" of just printing that JSON.stringify of the entire state in render was that is just didn't render properly when react refreshed after saving the code. Updating the page makes it all blank, but clicking onto another page through a link and then back fixes it. Dont know why, but I'll take it.