Issue using textarea with Recipe Box project

My recipe box basically works like this: you enter the name and ingredients, a button with the name of the food is created, clicking on the button will make a textarea with the ingredients come up (and clicking button again will make textarea disappear). The issue that I’m having is with changing the ingredients; ideally, the user should be able to change the content of the textarea, have the change saved to an array and then local Storage, then have that loaded back into the textarea when the page is reloaded. This works for the first button, but none of the others! I can’t fathom why this is.

I’m sorry if my code is a mess at all, but I’ve been trying a bunch of different things to no avail: alerts to make sure the for loop is working (it is), adding unique IDs to each “area” variable so that they can be called individually (doesn’t fix problem), and using addEventListener rather than on change (didn’t fix problem). Seriously stuck here, any help is appreciated: https://codepen.io/gphalen/pen/WJyrza

Hello, just taking a quick look…

First of all, yes, your code is a mess. Get in the habit of maintaining your code as you go. I am an inherently messy person (not dirty, just let things fall into disarray) … except when I code. Experience has taught me that this leads to nightmares. Codepen will auto indent for you if you select the code and shft-tab. Also, on the top right corner of the JS pane is a little down arrow for a pulldown menu that has some nice code cleaners. Tidy JS works nicely.

Opening up your console, I find some errors. (Please, please people, learn to read the browser console, it is your best tool, second only to coffee!)

For example, I get:

react-dom.min.js:14 Uncaught ReferenceError: evt is not defined
    at RecipeBox.handleChange (VM187 pen.js:126)
    at Object.executeOnChange (VM186 react-dom.min.js:12)
    at h.o (VM186 react-dom.min.js:13)
    at Object.r (VM186 react-dom.min.js:14)
    at a (VM186 react-dom.min.js:12)
    at Object.s [as executeDispatchesInOrder] (VM186 react-dom.min.js:12)
    at f (VM186 react-dom.min.js:12)
    at m (VM186 react-dom.min.js:12)
    at Array.forEach (<anonymous>)
    at r (VM186 react-dom.min.js:15)

This is a reference to these lines:

  handleChange(event) {
    this.setState({ [evt.target.name]: evt.target.value });
  }

You have to decide what to call this variable: evt or event.

After I fix that, the next error I get is:

VM385 react-dom.min.js:14 Uncaught TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.
    at RecipeBox.handleSubmit (VM187 pen.js:151)
    at Object.r (VM186 react-dom.min.js:14)
    at a (VM186 react-dom.min.js:12)
    at Object.s [as executeDispatchesInOrder] (VM186 react-dom.min.js:12)
    at f (VM186 react-dom.min.js:12)
    at m (VM186 react-dom.min.js:12)
    at Array.forEach (<anonymous>)
    at r (VM186 react-dom.min.js:15)
    at Object.processEventQueue (VM186 react-dom.min.js:12)
    at r (VM186 react-dom.min.js:14)

When I look in RecipeBox.handleSubmit I see:

  handleSubmit(event) {
    this.state.count++;
    var counter = 0;

    alert(
      "A name was submitted: " +
      event.target.food.value +
      event.target.ingredients.value
    );
    this.state.foodArray.push(event.target.food.value);
    this.state.ingredArray.push(event.target.ingredients.value);

    localStorage.setItem("text", JSON.stringify(this.state.foodArray));
    localStorage.setItem("text2", JSON.stringify(this.state.ingredArray));

    this.state.div.appendChild(event.target.food.value);
  }

Horror of horrors! Repeat after me: We must never, never change state directly unless we are in the constructor. Never. Repeat this 100 times while beating yourself with a stick so you don’t forget. If we were in the constructor, we could do things like this.state.count++; but anywhere else, it has to be (***HAS TO BE!!!***) done with this.setState. The way to do this is:

this.setState({ counter: this.state.counter + 1});

But you do this several times in this function so you can combine them all in one.

Then the line causing this last error:

this.state.div.appendChild(event.target.food.value);

Again, you never change state directly, but worse, you seem to be trying to do some jQuery-esque DOM manipulation here. But this is React - you don’t need to (and shouldn’t) be manipulating the DOM.

Looking back at your constructor, I see lines like:

      var btn = document.createElement("BUTTON"); // Create a <button> element
      var t = document.createTextNode(this.state.localStorageArray[y]);

You are missing the point of React. In React, you teach React how you want to reflect your state data (with JSX and helper functions) and then React handles the DOM.

So, you have a state array of this.state.foodArray. Then inside my JSX I would ahve something like

        <div>
          <p>
            {this.state.foodArray.map(food => (<button>{food}</button>))}
          </p>
        </div>

This will put all those buttons in there. How does it know when to update it? Because it checks everytime you change this.state with the setState function. This is the magic of React.

All of your DOM elements should be in your JSX, or in a rendering function inside your JSX or called from your JSX.

I haven’t had time to check this function out, just doing it off the top of my head, but it gives you the idea.

Just a quick look through your code in the constructor:

    for (var x in text1) {
      this.state.localStorageArray.push(text1[x]);
    }

Why are you pushing them onto an empty array? Why not just copy the whole thing? Or, you can do it in the initial constructor:

localStorageArray: JSON.parse(localStorage.getItem("text")),

when you define state originally.

I’m not even sure why you’re keeping a separate array for local storage. I see too many calls to local storage. To me, you should only read it once, for example in the constructor. It should be put into your state array for that variable. There is no need to get it again. And the only time you need to write to local storage is when you change the variable. You just stringify the recipe array and save it.

For that matter, I’d rethink how you’re storing the recipe. First of all, choose better variable names than text - variables should tell you what it is. If I were building this, I would have an array called this.state.recipes. Each of those recipes would be an object, like { name: "apple pie", ingredients: "flour, apples, sugar" }. So, this.state.recipes is an array of those objects. Then when I save those to local storage, I’d have a better key name, something like “gphalenrecipeapp”. If I look at local storage (which you can do in the dev tools, under “application”), you know exactly what it is.

There is a lot of information. I’m sure more problems will arise (they did for me when I was where you are!) but this should get you started.

Please let us know if you need clarification on any of this.

I created a simple example to help you understand some of these concepts:

class RecipeBox extends React.Component {
  constructor(props) {
    super(props);
    
    // some dummy recipes to start out
    const initRecipes = [
      { name: "pie", ingredients: "flour, apples" },
      { name: "meatballs", ingredients: "meat, flour, eggs" },
      { name: "shepheard's pie", ingredients: "meath, gravy, mashed potatoes" },
      { name: "ceviche", ingredients: "fish, onion, lemon juice" },
      { name: "suffing", ingredients: "bread cubes, chicken stock, celery, onions" },
      { name: "pizza", ingredients: "crust, sauce, cheese, pepperoni" }
    ]
    
    // put them in state
    this.state = {
      recipes: initRecipes
    }
 
    // bind *this* to our class method
    this.handleRemove = this.handleRemove.bind(this);
  }
  
  handleRemove() {
    // create a new copy of the recipes so we can change it
    let newRecipes = this.state.recipes.slice()
    // remove a random recipe
    newRecipes.splice(Math.floor(Math.random()*this.state.recipes.length), 1)
    // put it into state
    this.setState({ recipes: newRecipes })
  }

  render() {
    
    // just a simple way of changing out array of data into an array of JSX
    // I could have put it directly in the JSX, but this is cleaner
    const renderRecipes = this.state.recipes.map(recipe => (
      <div className="recipe">
        <span className="recipe-name">{recipe.name}:</span> &nbsp;
        <span className="recipe-ingredients">{recipe.ingredients}</span>    
      </div>
    ))
    
    return (
      <div>
        <div>
          {renderRecipes}
        </div>
        <br />
        <button onClick={this.handleRemove} disabled={this.state.recipes.length===0}>
          Remove random recipe!
        </button>
      </div>
    )
  }
}
ReactDOM.render(<RecipeBox />, document.getElementById("root"))

Notice that at no time do I change the state outside of the constructor without using setState. NEVER! And notice that I don’t have to manipulate the DOM - React does it for me. I told React how to convert that array in state into JSX and it will automatically change it is that state array changes.

The pen is here.

2 Likes

OK wow, thank you, that is thorough. Yeah, sorry for the messiness. I’m a messy person too, I was better at organizing my code in school when I had a grade hinging on it, but it’s easier to let my inclination toward disorganization take over when I do independent projects. I’m also still new to ReactJS, so it’s very tempting for me to just try vanilla JS solutions when I get stumped. Also new to localStorage, which is why my employment there is a bit clumsy.

I’ve implemented your changes, but now nothing happens when I click the buttons, and nothing shows up in the error console so I’m not sure what the issue is. I mean, I know it’s still not written as optimal ReactJS, but I just don’t see any glaring issues that would prevent the code from running as intended: https://codepen.io/gphalen/pen/gzKKdE

I feel your pain. I remember trying to learn React. It is a big leap from vanilla JS and jQuery.

But I still see things like:

this.state.areaArray.splice(y, 0, area.value);

this is mutating state. That is a huge no-no and will burn you later - if it isn’t now.

I also see a lot of direct DOM manipulation/accessing through the document object. There should be none of that. Period. The only place you should do that is in the final line of the code.

ReactDOM.render(<RecipeBox />, document.getElementById("root"));

That’s where you connect React to the DOM. If you do that, then React is in charge of the DOM now. Period.

I think you need to take a step back. Restart. You can even use what I made as a starting point. Take it step by step. See if you can get it to add recipes, the React way. Do some research. Don’t worry about saving to local storage right now - if everything else works, that will be easy to add. Just load some dummy data to start. I also gave some advice on how to structure your data.

Make small changes and test. If you want, check back with the forum from time to time to see if you are using React correctly.

I know it seems very draconian, but React is a very powerful tool, but must be used in a certain way.

If you have any questions, just ask the group. That’s why the forum is here. I guarantee, whatever question you have, there are at least a dozen other people with the same question that are too chicken to ask.

By the way, your button:

<button onclick={this.clickEvent}>{food}</button>

isn’t working because in React, it is spelled “onClick”. Yeah, I make that mistake at least once a day.

Of course that function also needs to have this bound in the constructor.

But as I said, you’ve got several conceptual problems going on. I think you need to step back, start more simply, and build from there.

Another thing to consider - how about having different components?

App --|
      |
      |-- Header
      |
      |-- Controls
      |
      |-- RecipeBox
      |     |
      |     | -- Recipe
      |
      |-- Footer

The ability to compartmentalize sections of the screen into React components is one of its strengths. In this case, the RecipeBox components has a bunch of Recipe subcomponents. This is more inline with the React way of doing things. See if you can change the code I gave you to something like that. One gotcha to keep in mind - one that drove me crazy until I found out - each component has its own state. You will keep your state for global things in a component like RecipeBox and then you will pass the data around. A subcomponent might use its own state for something, like the value of an input that is just for that component’s use.

1 Like

Alright. I don’t wanna give my life story, but basically: I started this particular project roughly a year ago, then I was working out of tech for a while, got back on the programming train around March of this year, went through the “Intro to React” tutorial* and finished their Tic Tac Toe game, came back to the recipe box and have been stuck on it over a month. I don’t think I’ve ever been this vexed by a project, I did more conceptually complicated stuff in college, and even for the FCC Front End certification, but I just can’t get this seemingly simple project right. It felt like I was getting close, but I know I really wasn’t.

Sooo, with all that said, the notion of starting over again is a bit discouraging. But you seem to know your stuff, and I’ll defer to your judgment.

*https://reactjs.org/tutorial/tutorial.html#wrapping-up

Yes, but React is very different, conceptually. You are not exactly starting from scratch, but there are a few conceptual hurdles that will drive you crazy in the beginning. Once you get them, they will be easy, but until then …

But the path you are going down is not the right one. Even if you got it working (which I question) it wouldn’t be a true React app.

I myself came at this with a coding background, so I understand the “but I should be getting this” feeling.

But I still feel like you would be much better served by taking a step back and start over and build things the React way.

If you aren’t sure about something, PM me. But most questions would probably serve the forum well. And if you search, you may find them answered.

I had another codepen example showing data passing, but I seem to have overwritten it. I’ll rebuild it and post it.

Here is the data passing example I mentioned.