1

I am making an app in which I have 3 buttons of which 2 are of importance to my problem. One button should add elements to the website the user is viewing and the other should remove elements.

I'm new to React and feel I have a grasp of the basics but clearly need to get my head around it better.

My first intuition was to just create an Array into which I could push elements and then just have that array in one of my JSX.Elements as the content of it.

That looked something like this:

My JSX.Element:

   answerView = (
                <div>
                    <button onClick={this.removeIceCream}>Poista jäätelö!</button>
                    <button onClick={this.addIceCream}>Lisää jäätelö!</button>
                    <button onClick={this.keyInput}>OK</button>
                    <div>{this.iceCreamCount}</div>
                </div>
            );

The function to just add something to the element.

addIceCream() {
   this.iceCreamCount.push(<li>"Jäätelö"</li>);
}

However this ends up in the array iceCreamCount updating but not being displayed in the DOM. Does React not ehmm "react" to changes in elements and only changes in its State or Properties?

Does this mean that in order to manipulate the DOM elements in accordance to user input my array containing the elements also needs to be a state variable?


If the above is indeed impossible then I would like help with how to do it properly in React and below is my attempt at doing so.

I've checked a few tutorials and examples and came up with something like this:

  addIceCream = () => {
        this.setState(state => {
            const iceCreams = state.iceCreams.concat(state.iceCream);

            return {
                iceCreams,
                iceCream: 'Jäätelö',
            };
        });
    };

With my state declaration looking like this:

this.state = { doorView: true, bunnyView: false, answerView: false, iceCream: "Jäätelö", iceCreams: [] };

During writing this problem I actually got this seemingly working but I would also like to understand how and why this works as I am not really confident in my knowledge.

However I don't feel like I've grasped what exactly is happening here. I'm not very confident with arrow-functions (if anyone has a great simple tutorial on them please do link) and while I think I've gathered that because data is supposed to be immutable in React is why I need to use concat() to create a new array that is a copy of the old one + my added element. But why do I need to return() the state variables and why are there so many arrow functions being thrown around (see below for my condensed "what I would like to know list")?


What I would like to know

  • Is the first part of my question a workable way in React or is it simply the wrong approach completely? If it can be done, I would appreciate if someone would show me how and what are my mistakes in my thinking.
  • Why and how does the second part of my question work? If someone had the time and patience to go through it line by line starting from: addIceCream = () => {.... and tell me what is happening that would help me a lot in understanding and learning.
KsK
  • 167
  • 1
  • 2
  • 8
  • 1
    It seems like you want a React tutorial, as this is *fundamental* to React, and in pretty much every tutorial ever (e.g., the ubiquitous Todo Apps that abound). The "how and why" is a rabbit hole, better served by, again, a tutorial, supplemented with the React docs. Regarding arrow functions: also dozens or hundreds of resources available on the web. Due diligence: do the work. – Dave Newton Aug 02 '19 at 13:14
  • Thanks for your answer. A refresh on a ToDo guide might be a good idea as it's been a few months since I did them. On arrow functions: I am aware that there are alot of tutorials out there and I've read more than a few. Since I still struggle I was hoping maybe someone here might have a way of explaining it or a link to a source that they found helpful, that might help me. "Do the work" comes across in this context to me as implying if someone doesn't understand something they haven't tried to (maybe not what you meant). All I was looking for was a possible alternative to help me understand. – KsK Aug 02 '19 at 13:43
  • No, that's pretty much what I meant. There's almost nothing to understand about them--they're a function where `this` is bound to the execution context in which the function is defined; in React they're an alternative to using `bind`. This is explained in (often exhaustive) detail all over the place. Saying "you're struggling" isn't diagnostic: there's no way for anyone to understand where/how you're struggling, so there's no way to know how to explain it any differently. This isn't a criticism per se--but it is an exhortation to (a) be specific, and (b) try harder with existing resources. – Dave Newton Aug 02 '19 at 13:48
  • A question I always share when functions and context (`this`) is mentioned: [How to access the correct `this` inside a callback?](https://stackoverflow.com/q/20279484/1218980) – Emile Bergeron Aug 02 '19 at 18:04

3 Answers3

3

I will answer questions below.


Answer to the 1st question

Is the first part of my question a workable way in React or is it simply the wrong approach completely? If it can be done, I would appreciate if someone would show me how and what are my mistakes in my thinking.

The first approach is "the wrong approach completely" unfortunately (in React world that is).

addIceCream() {
   this.iceCreamCount.push(<li>"Jäätelö"</li>);
}

However this ends up in the array iceCreamCount updating but not being displayed in the DOM. Does React not ehmm "react" to changes in elements and only changes in its State or Properties?

That's exactly it. React "reacts" to the state & props changes that React keeps track of.

Does this mean that in order to manipulate the DOM elements in accordance to user input my array containing the elements also needs to be a state variable?

It depends. Some controls are "controlled" and another "uncontrolled" (, which applies to mostly to form fields.)

Most of the time, you'd go with (React) "controlled" options to keep all your states associated with React to keep track of. (In Class Components this.state ={...} or in Function Component using a hook, const [state, setState] = React.useState(...))

React's Reconciliation (a fancy way of saying, knowing what's changed and what to render) algorithm works by checking changed state/prop "references" (as opposed to a value).

When you did this.iceCreamCount.push(<li>"Jäätelö"</li>), you are basically changing the value of an array, "this.iceCreamCount", not the reference thereof.

And also after reassigning this.iceCreamCount to a new object, you also need to this.setState({iceCreamCount: newIceCreamCount}) to notify React that something has changed and need to re-render.

Now we know why the first approach doesn't work, let's move onto your workaround.


Answer to the 2nd question

Why and how does the second part of my question work? If someone had the time and patience to go through it line by line starting from: addIceCream = () => {.... and tell me what is happening that would help me a lot in understanding and learning.

state declaration

this.state = {
  doorView: true,
  bunnyView: false,
  answerView: false,
  iceCream: "Jäätelö",
  iceCreams: []
};
addIceCream = () => {
  this.setState(state => {
    const iceCreams = state.iceCreams.concat(state.iceCream);

    return { iceCreams, iceCream: "Jäätelö" }
    };
  });
};

When you look at addIceCream, you are indeed creating a new reference, iceCreams.
As explained in Question #1 above, that's how React's reconciliation algorithm know that a state has changed.
The gist is that React is optimized to check for the reference change because a deep property check (suppose that this.state has a deeply nested objects, then comparing each value would take too long)

And at the end you are returning a new reference return { iceCreams, iceCream: "Jäätelö" }, which notifies React that something indeed has changed.


Following up with the comment.

how do the arrow functions here work and why they are required for the "addIceCream"-function to work?

You've declared addIceCream using an Arrow Functions syntax.
As mentioned in the section below, No Separate this, an arrow function does not create its own this variable.

But when you do, this is set to parent context's this due the magic of scoping rules.

addIceCream = () => {
  this.setState(state => {
    const iceCreams = state.iceCreams.concat(state.iceCream);

    return { iceCreams, iceCream: "Jäätelö" }
    };
  });
};

That's why you can call this.setState above. JavaScript engine will try to find the closest parent's this up in the scope chain.

If you had declared addIceCream as a normal method,

class App extends React.Component {
  addIceCream() { ...}
}

then this points to addIceCream thus, won't have access to App's this.setState, which is made available from React.Component when you extend it.

So you'd see a workaround to bind the class method by passing its own this to it, which create a new function with current class's this.

class App extends React.Component {
  constructor(props) {
    super(props)

    //                     ... this create a new method with `App`'s `this`.
    this.addIceCream = this.addIceCream.bind(this)
  }

  addIceCream() { 
    // now that `this` is bound to `App`'s `this`, you can call this
    this.setState({...})
  }
}

This would be applicable when you use a Class Component (CC). When you use a Function Component (FC), this wouldn't matter.

You can use React's new Hooks (useState, or useReducer) to save the state.

(use useReducer for a complex state management)

cosnt initialState = {
  doorView: true,
  bunnyView: false,
  answerView: false,
  iceCream: "Jäätelö",
  iceCreams: []
};

function reducer(state, action) {
  switch (action.type) {
    case 'ADD_ICECREAM':
      const { iceCream } = action.payload
      const iceCreams= [...state.iceCreams].concat(iceCream)
      return { ...state, iceCream, iceCreams }
    default: return state;
  }
}

function App() {
  const [icecreams, setIceCreams] = React.reducer(reducer, initialState);
  // You can use either one now.
  const addIceCream = iceCream =>
    dispatch({type: 'ADD_ICECREAM', payload: { iceCream }});
  // OR
  function addIceCream(iceCream) {
    dispatch({type: 'ADD_ICECREAM', payload: { iceCream }});
  }
}

reducer function returns a new object reference thus will cause React to re-render.

You might have noticed that arrow syntax version uses const like const addIceCream = iceCream => ....

Now it becomes a pointer to a function, so it should be declared before its being used unlike the function version because of how JavaScript hoisting works.

dance2die
  • 35,807
  • 39
  • 131
  • 194
  • 1
    Might be good to mention also that setState() is asynchronous. Beginners tend to update state and try something immediately afterward and get unexpected results not realizing this. – Mark Aug 02 '19 at 13:50
  • Thanks for your clear, to the point answer. The answer to my first question specifically literally answered what I wanted to know with helpful comments and a friendly tone to boot! I would be interested to know if you could be bothered to explain in the same manner how do the arrow functions here work and why they are required for the "addIceCream"-function to work? If not what resources/tutorials would you recommend for someone who struggles to grasp the concept of arrow functions? – KsK Aug 02 '19 at 13:54
2

Is the first part of my question a workable way in React or is it simply the wrong approach completely? If it can be done, I would appreciate if someone would show me how and what are my mistakes in my thinking.

If you are using classes, you can use this (the instance of the component) to store data. However, if you are using this data in your render function, you most likely want to go for this.state.

Why and how does the second part of my question work? If someone had the time and patience to go through it line by line starting from: addIceCream = () => {.... and tell me what is happening that would help me a lot in understanding and learning.

Here you go:

  // Declare a method
  addIceCream = () => {
        // calling `setState` instructs the Component class that you are 
        // going to update the state. This way, the Component class knows 
        // that it should see if there are changes and it should rerender.
        // When calling `setState` with a function, you get the current state
        // as the first argument.
        this.setState(state => {
            // This creates a new `iceCreams` Array with the `state.iceCream`
            // as new value. You shouldn't call `push` here because this mutates
            // the original array and can be seen as antipattern. 
            const iceCreams = state.iceCreams.concat(state.iceCream);

            // Returning the changes to the state. The `iceCream` property is 
            // probably wrong here
            return {
                iceCreams,
                iceCream: 'Jäätelö',
            };
        });
    };
Chris
  • 6,331
  • 1
  • 21
  • 25
  • Thanks for your answer! To your first comment: My idea initially was to have an array declared inside my class, then display that array inside my render (as shown in my first code snippet {this.iceCreamCount}) and then just have a function that is fired everytime the "add ice creams" button is clicked (first picture as well). I then just added an
  • element with some text representing my content to that array (second picture). However my problem was that the elements being added via the button click were never rendered and I was wondering why that was?
  • – KsK Aug 02 '19 at 13:25
  • To your second comment: Thanks for the clarification I got some good tidbits out of it. However since what I want (for now) is to add the string "Jäätelö" (ice cream in Finnish) to my DOM everytime the button is clicked, I thought I needed to also return that property as well (iceCream: 'Jäätelö'). I know realized that since I've already defined this in my state declaration and unlike the array iceCreams I did not modify it that was a redundancy. The hardest part for me to understand is the arrow functions and why they are used here and what to they do. I've read tutorials but still struggle. – KsK Aug 02 '19 at 13:32
  • Woops didn't see this answer. Good answer +1 – dance2die Aug 02 '19 at 13:34
  • @KsK React doesn't know/care about, your component's instance properties. React knows/cares about state and props. This is why in every React tutorial data used during rendering is in state and/or props. An arrow function is used here so `this` is bound to the component instance, allowing referencing `this.state`. Without an arrow function `this` would be late-bound to whatever execution context it's in (I forget what) unless `addIceCream` is bound via `bind`, generally done in the constructor. This is in [the React docs regarding events](https://reactjs.org/docs/handling-events.html). – Dave Newton Aug 02 '19 at 13:52