DEV Community

loren-michael
loren-michael

Posted on

Recipe Manager 2.0: React!

Over the past few weeks I've been learning React, and now it's time to show what I've learned. I decided to make a recipe manager similar to the one that I previously built using vanilla JavaScript. While it was relatively easy to make this transition, I definitely encountered some hiccups that required a little more troubleshooting than I anticipated.

After setting up my project and building my components, I made sure they would render by lining them up in my App and checking them in my browser. I then wrote a useEffect to fetch the recipes from my JSON database and stored that information in state so that any recipe can be rendered using a single fetch. Next, I started to distribute props and added Routes to the components. Right away I knew there was something wrong. The issue I encountered stemmed from using incompatible versions of React and React Router. Of course I figured out where the problem was after I wrote all of my Routes! Because I had installed v18 of React, I had to update my React Router from v5 to v6 and update all of the syntax around my Routes. Ultimately, updating the syntax didn't take very long and in the long run the new version looks much cleaner, so I'm actually glad I ran into this issue and learned a new and updated way of Routing.

From there, I was able to build out a home page using Semantic UI Cards. Each card shows a picture, the recipe title and whether or not the recipe is one of my favorites. Clicking on a recipe title will take you to the recipe's details page, where ingredients, instructions and any comments are displayed. Here is where you can add a comment or favorite/unfavorite a recipe.

This is where I ran into a common issue when using state in React. When updating state within a function, I would often try to utilize the updated state before the function finished and the changes were actually applied within the component.

For example, instead of changing whether or not a recipe was a favorite just by setting the "favorite" state:

function handleFavorite() {
    const newFavorite = !favorite;
    setFavorite(newFavorite);
};
Enter fullscreen mode Exit fullscreen mode

I used a callback function within my setState hook:

function handleFavorite() {
    setFavorite(function (favorite) {
        const newFavorite = !favorite;
        return newFavorite;
    })
};
Enter fullscreen mode Exit fullscreen mode

I then paired this function with a useEffect hook that is called whenever the "favorite" state is changed. Within the useEffect, the new "favorite" status gets PATCHed to the recipe database to make sure it is always current. At this point, the "recipes" state that is stored is no longer current, so I have the useEffect also fetch the updated database to store in the "recipes" state.

useEffect(() => {
     fetch(`http://localhost:3000/recipes/${recipe.id}`, {
        method: "PATCH",
        headers: {
           "Content-Type": "application/json"
        },
        body: JSON.stringify({"favorite": favorite})
     })
     .then(fetch(`http://localhost:3000/recipes`)
        .then(r => r.json())
        .then(recipes => {
           setRecipes(recipes);
}))
}, [favorite])
Enter fullscreen mode Exit fullscreen mode

I used a similar process for the comments section, so that when a comment is submitted to the recipe, it updates the state of the "comments" array, which triggers a fetch within a useEffect that patches the new array to the database and then fetches the recipes to save into the "recipes" state to keep current with the database.

To set up all of these inputs as controlled inputs, I looked at my database and created a newRecipe state that had all of the keys that I wanted to include in the form. This includes things like the name of the recipe, the author, website, a photo URL, etc... When I got to the keys whose values were arrays, I simply included an empty array or, in the case of the comments, the value was assigned as another state. Take a look:

const [newRecipe, setNewRecipe] = useState({
    img: "",
    video: "",
    name: "",
    source: "",
    author: "",
    mealtype: "",
    preptime: "",
    cooktime: "",
    servings: "",
    ingredients: [],
    instructions: [],
    comments: commArr
});
Enter fullscreen mode Exit fullscreen mode

From here, I made all of the single string inputs controlled by one function to update the values for those items in the newRecipe state. I had to be a little creative with the ingredients and instructions, because recipes don't have a set number of ingredients or instructions to include in a form like this. I couldn't just throw in 5 inputs for ingredients and 5 inputs for instructions. I wanted to be able to click a button and add a new input that would then be included in the new recipe's state. To do this, I wrote a function that would update a state array that simply had numbers in it that would act as my keys later on.

const [numIng, setNumIng] = useState([0, 1, 2, 3, 4]);

function handleAddIng() {
    const newNum = numIng.length;
    setNumIng([...numIng, newNum], () => {});
};
Enter fullscreen mode Exit fullscreen mode

Once I had that functioning properly I took that state array and mapped it to render one input for each value in the array, using the value as a key. Once the state array updates with a new number, a new input is added to the page with a proper key, className and onChange function for the input to be controlled.

{numIng.map((num) => {
    return (
        <div>
            <input type="text" key={num} className="add-ingredient" onChange={handleIngredients}></input>
        </div>
    )
})}
Enter fullscreen mode Exit fullscreen mode

Then, to make sure these inputs are also controlled and are being stored in the new recipe state object, I wrote a function to keep the array updated. I had to keep in mind that retrieving elements this way gives an HTML collection, and not an array that I can iterate through in the way I wanted, so I used a spread operator to convert the data from a collection to an array that I could use. I then filter out any of the inputs that don't have any text in them and store the resulting array in the new recipe state object.

function handleIngredients() {
    const ingElements = document.getElementsByClassName("add-ingredient");
    const convIng = [...ingElements];
    const newIngArr = convIng.filter((ing) => ing.value.length > 0).map((ing) => ing.value)
    console.log(newIngArr);
    setNewRecipe({...newRecipe, ingredients: newIngArr});
}
Enter fullscreen mode Exit fullscreen mode

Recipe Manager 2.0 is now functioning the way that I want it to - at least for now. In the future I plan on adding functionality that will display recipes based on an ingredient search, rather than only searching by recipe name. I would also like to filter by tags and include embedded videos from the recipe's author if one is available.

Discussion (0)