Iam writing a cookbook app. The recipes of the cookbook are stored in yaml files and these are being stored in a static way. When I load up the site, it will automatically reach out to an index.json file in which all recipes are indexed and load them one after one and add them to an array. This array is then given to the setRecipe method where it should update the dom accordingly. This doesn't happen. I already tried to console.log a little and when doing this I get logged the expected data but as soon as I refresh the page this isn't case anymore. The request for the yaml files are being done. Why does that happen?
Full Sourcecode
import React, { useState, useEffect } from 'react';
import jsyaml from 'js-yaml'
import { ListGroup, ListGroupItem } from 'react-bootstrap';
const basePath = '/k0chbuch'
const recipeStore = basePath + '/recipe-store'
const index = recipeStore + '/index.json'
const RecipeList = () => {
const [recipes, setRecipes] = useState([]);
useEffect(() => {
fetchAllRecipes();
}, []);
const fetchAllRecipes = () => {
fetch(index)
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json()
})
.then(recipeIndex => {
let store = []
recipeIndex.forEach(element => {
fetch(recipeStore + '/' + element + '.yaml')
.then(res => res.blob())
.then(blob => blob.text())
.then(text => jsyaml.load(text))
.then(recipeObject => {
store.push(recipeObject)
})
})
return store
})
.then((all) => setRecipes(all));
}
return (
<div>
<h1>Rezepte:</h1>
<ListGroup>
{recipes.map((r)=>(<ListGroupItem>{r.Titel}</ListGroupItem>))}
</ListGroup>
</div>
);
};
export default RecipeList;
Simple Yaml example:
---
Titel: Hackbraten
Autor: d3v3lop3r
Komplexitaet: 1
Portionen: 4
Zutaten:
- 1000g Hack
- 150g Zwiebeln
- Gewürze nach Wahl
Zubereitung: |
Zuerst wird das Hack gewürzt, dann die Zwiebeln braten und dem Hack zugeben. Kräftig kneten, dann bei 200°C eine Stunde backen.
Kommentar: Schmeckt am besten mit Kartoffeln!
Your issue comes with populating the store
array in your second .then()
: when using Array.prototype.forEach
, remember that return
doesn't really do anything. You are not awaiting for the blob to be parsed before resolving the promise. In fact, this section of the daisy chained promises resolves immediately:
.then(recipeIndex => {
let store = [];
recipeIndex.forEach(element => {
fetch(...);
});
// `store` is returned immediately without waiting for forEach!
return store;
})
Instead, I would suggest using Array.prototype.map
to return an array of promises based off recipeIndex
. Then return Promise.all()
, which ensures that all promises are resolved:
fetch(index)
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
})
.then(recipeIndex => {
const promises = recipeIndex.map((element) => {
return fetch(recipeStore + '/' + element + '.yaml')
.then(res => res.blob())
.then(blob => blob.text())
.then(text => jsyaml.load(text));
});
return Promise.all(promises);
})
.then((all) => setRecipes(all));
for
loop? (You can, but not recommended)An alternative is to use a for
loop, which you can then use async/await. However I would typically not suggest this for two reasons:
for
loop + await
means that each requests are dispatched after each other and not in parallel, which increases latency. The Array.prototype.map
solution above dispatches fetch requests at the same time..then()
However if you want to give it a try, a for
loop is totally doable:
fetch(index)
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
})
.then(async (recipeIndex) => {
const recipes = [];
for (const element of recipeIndex) {
const recipe = await fetch(recipeStore + '/' + element + '.yaml')
.then(res => res.blob())
.then(blob => blob.text())
.then(text => jsyaml.load(text));
recipes.push(recipe);
}
})
.then((all) => setRecipes(all));
useEffect
runs based on the dependency array that is passed as the second argument of the useEffect
hook.
Try this,
const [recipes, setRecipes] = useState([]);
useEffect(() => {
if(!recipes && recipes.length == 0){
fetchAllRecipes();
}
}, [recipes])
This will cause your useEffect
hook to call as soon as the recipes
state changes but will only cause the fetchRecipes()
function to call if the recipes
state is null
or the length of the array is 0