0

I'm following Pure React, and there's a House component that needs some building (omitted the imports for brevity):

class House extends React.Component {

    state = {
        bathroom: true,
        bedroom: false,
        kitchen: true,
        livingRoom: false
    }

    flipSwitch = (action) => {
        this.setState({
            ???????????????
        });
    }

    render () {
        return (
            <>
            <RoomButton room='kitchen' handler={this.flipSwitch}/>
            <RoomButton room='bathroom' handler={this.flipSwitch}/>
            <RoomButton room='livingRoom' handler={this.flipSwitch}/>
            <RoomButton room='bedroom' handler={this.flipSwitch}/>
            </>
        );
    }
}

const RoomButton = ({room, handler}) => (
    <button onClick={handler}>
        {`Flip light in ${room}!`}
    </button>
)

ReactDOM.render (
    <House/>,
    document.getElementById('root')
)

Desired outcome: when you click the <room> button, the state of the House component changes to reflect the flipping of the lightswitch in the room (i.e. true is light on, false is light off).

I'm wondering what should go instead of the ????? - how do I pass the room prop into the handler function from inside a given RoomButton component?

Tried just passing it in, and got a Maximum depth reached error from React.

ES6 answers would be most appreciated, BTW.

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
Tom Granot
  • 1,840
  • 4
  • 22
  • 52
  • 2
    Presumably the error was because you wrote `handler={this.flipSwitch("kitchen")}`? – jonrsharpe Aug 27 '19 at 18:39
  • @jonrsharpe yep! I figured it was some sort of (React's version) of a stack overflow (hehe), but wasn't really sure how to decipher it and come up with an actionable solution. – Tom Granot Aug 27 '19 at 18:47
  • Then it's because you're calling the function and passing the *result* as the handler. You need to "defer execution", note that you didn't start with `handler={this.flipSwitch()}`... – jonrsharpe Aug 27 '19 at 18:50
  • @jonrsharpe OK! Thanks a lot! – Tom Granot Aug 27 '19 at 18:52
  • Possible duplicate of [Callback Function With Parameters ReactJS](https://stackoverflow.com/questions/45359113/callback-function-with-parameters-reactjs) – Emile Bergeron Aug 27 '19 at 19:25

2 Answers2

5

You can try this solution :

the flipSwitch will accept the room name as parameter and will update the state using a callback (in order to retrieve the right value of the current state)

 class House extends React.component {
 ....

   flipSwitch = room => {
     this.setState(state => ({
        [room]: !state[room]
     });
   }

 ...
 }


const RoomButton = ({room, handler}) => (
  <button onClick={() => handler(room)}>
    {`Flip light in ${room}!`}
  </button>
);
Olivier Boissé
  • 15,834
  • 6
  • 38
  • 56
  • Olivier - so you pass an anonymous arrow function? Not sure I see how this works. – Tom Granot Aug 27 '19 at 19:06
  • 1
    yes it's simply a function. In the accepted answer it uses `bind` which actually also create a function – Olivier Boissé Aug 27 '19 at 19:07
  • 1
    In a world where arrow functions are available, I'd use them over `.bind` as it way clearer what the intent is and there is no risk to mess up the context that way. – Emile Bergeron Aug 27 '19 at 19:11
  • yes I think this answer is better than the accepted one, in addition to that its preferable to use a callback in `setState` when the next state value depends of the current state otherwise if you click really fase twice on the same button, you could have an undesirable behavior as `setState` is asynchronous – Olivier Boissé Aug 27 '19 at 19:13
  • _"its preferable to use a callback in `setState`"_ It really depends on what's the expected behaviour. Say OP only wants it to switch once even if called multiple times, then a shallow merged object would be ok. But it's definitely good to be aware of the side-effects of both techniques! – Emile Bergeron Aug 27 '19 at 19:19
  • 1
    This is the more idiomatic way to do it in React, as far as I've seen. The `bind` way still works fine though. – Dave Ceddia Aug 27 '19 at 19:41
3

Try this

flipSwitch = (room, action) => {
    this.setState((prevState) => ({
        [room]: !prevState[room]
    }));
}

const RoomButton = ({room, handler}) => (
    <button onClick={handler.bind(null, room)}>
        {`Flip light in ${room}!`}
    </button>
)

The .bind(null, room) line essentially preloads the handler function with one of the arguments, in this case room. This is called currying. I left action in the flipSwitch parameters list in case you had plans for it, but it's unused currently.

John Ruddell
  • 25,283
  • 6
  • 57
  • 86
Jeff Hechler
  • 285
  • 3
  • 16
  • 1
    You're missing the square brackets `[room]:` and now the `action` variable is useless. But otherwise, this is correct. – Emile Bergeron Aug 27 '19 at 18:42
  • @JeffHechler Ah! Yes, I've tried this but totally forgot to bind `room` to the handler. Why the `null` there, though? – Tom Granot Aug 27 '19 at 18:50
  • you dont want to change the context of the function. `this.setState` would be undefined if you changed that null to this. Essentially it means dont touch the `this` context of that function. – John Ruddell Aug 27 '19 at 18:51
  • the null argument is in the place of `this`, preventing a replacement of the `this` keyword's context. By passing in null, the context is preserved. – Jeff Hechler Aug 27 '19 at 18:52
  • Alright, that makes sense. Thanks a bunch to all three of you! – Tom Granot Aug 27 '19 at 18:53
  • @john flipSwitch is an arrow function, so the context does't change with bind. – Emile Bergeron Aug 27 '19 at 18:53
  • When using `.bind` or `.apply` on an arrow function, the first argument (the context) is completely irrelevant. You could pass anything and it would still work with the right `this.setState`. – Emile Bergeron Aug 27 '19 at 19:07