2

What is the recommended/cleanest way to dynamically change an event's binding in react?

For example, if I initially have a button like this

<button type="button" onClick={this.handleFirstClick}>

Then in the handleFirstClick method

handleFirstClick() {
    //do other stuff
    //rebind the button, so that next time it's clicked, handleSecondClick() would be called
}

In case it's not entirely clear what I mean, here's what I'd like to do, using jQuery instead of React

$('#myButton').on('click', handleFirstClick);

function handleFirstClick() {
    //other stuff
    $('#myButton').off('click');
    $('#myButton').on('click', handleSecondClick);
}
1
  • I don't know if it's the proper way, but I would have a state counting the number of clicks on that button and have handleClick() call the proper functions accordingly. Commented May 31, 2017 at 18:23

5 Answers 5

1

Solution 1: React State and Ternary Expressions

In order to change the event's binding, you'll need to have a if-else within the render method. Create some state for the component to handle whether the button has been clicked yet. After the first click, set the state so that in the future the second function will be run. You can include a basic ternary expression to check the state in your render().

class FancyButton extends React.Component {
  constructor() {
    super()
    this.state = {
      clicked: false
    }

    //Bindings for use in render()
    this.onFirstClick = this.onFirstClick.bind(this)
    this.onSecondClick = this.onSecondClick.bind(this)
  }

  onFirstClick() {
    console.log("First click")
    this.setState({
      clicked: true
    })
  }

  onSecondClick() {
    console.log("Another click")
  }

  render() {
    return ( <
      button onClick = {
        this.state.clicked ? this.onSecondClick : this.onFirstClick
      } > Click < /button>
    )
  }
}

ReactDOM.render( < FancyButton / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Solution 2: Persisting State in an Object Property

In a more general sense, you may not need to change the event handler in your render method. If you just call one onClick handler and toggle an object property, you can skip rerendering the react component after each click (due to skipping the call to this.setState).

class FancyButton extends React.Component {
  constructor() {
    super()
    this.clicked = false

    //Bindings for use in render()
    this.onClick = this.onClick.bind(this)
  }

  onClick() {
    if (!this.clicked) {
      this.onFirstClick()
    } else {
      this.onSecondClick()
    }
    this.clicked = true
  }

  onFirstClick() {
    console.log("First click")
  }

  onSecondClick() {
    console.log("Another click")
  }

  render() {
    return ( <
      button onClick = {
        this.onClick
      } > Click < /button>
    )
  }
}

ReactDOM.render( < FancyButton / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

I would personally recommend the second solution, as it is more efficient, but you can decide on your own which fits your situation the best.

Sign up to request clarification or add additional context in comments.

1 Comment

Although this doesn't exactly answer the question, I'm making it as accepted because it's clean and works. And it's highly possible the answer to my question is, "that's not what you do in react"
1

Create a function that watches the state and determines which function should be ran.

handleClick() {
    if (this.state.clickNumber === 1) {
        this.handleFirstClick()
    } elseif (this.state.clickNumber === 2) {
        this.handleSecondClick()
    }
}
handleFirstClick() {
    //do stuff
    this.setState({clickNumber: 2})
}
handleSecondClick(){
    //do other stuff

    // maybe increment click number again this.setState({clickNumber: 3})
}

Comments

1

Just keep a counter of how many times button is clicked, and then use it to assign a handler. See this snippet,

class App extends React.Component{
	constructor(props){
		super(props)
		this.state={
			clicked: 0
		}
	}
	firstClick(){
		console.log("first click")
		this.setState({clicked: 1})
	}
	afterClick(){
		console.log("more clicks")
	}

	render(){
		return(
			<div><button onClick={this.state.clicked === 0 ? this.firstClick.bind(this) : this.afterClick.bind(this)}>Click me</button>
			</div>
		)
	}
}

ReactDOM.render(<App />, document.getElementById("app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

Comments

1

You could use a closure.

const createClickHandler = () => {
  const firstClickHandler = () => {}
  const secondClickHandler = () => {}
  let clicked = false

  return () => {
    if (clicked) return secondClickHandler()
    clicked = true
    firstClickHandler()
  }
 } 

Comments

1

In React, you'd be best off using state to manage these events. You can hold the state and actions in the individual button components, or in the parent component that renders the button components.

In set this up the button components utilize a binary state that toggles between click one and 2 functions with a Boolean value. This could also easily change to function two and stay there by modifying the buttons's handleClick setState call, or keep iterating to additional functions - or even call the same function with an updated input each click (depending on your use case).

In this example, I decided to show how this would work in a stripped down React App. In this example you'll notice that I keep the state in each button. It's doubtful that another component will need to track the click count, so this follows the react principal of pushing the state down to the lowest possible component. In this case, I allowed the parent component to hold the functions that would act on the button state. I didn't see a necessity in these functions and their overhead also being duplicated with each button component, but the button components certainly could have called the functions themselves as well.

I added two buttons to show how they will individually maintain their state. Forgive the comment block formatting, it's not displaying well outside of the snippet window.

/**
	*		@desc Sub-component that renders a button
	*		@returns {HTML} Button
	*/
class BoundButton extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);

    this.state = ({
      //toggled in this example to keep click state
      click2: false,
    });
  }
  
  handleClick(e) {
    //calls parent method with the clicked button element and click state
    this.props.click(e.nativeEvent.toElement, this.state.click2);
    
    //toggles click state
    this.setState({ click2: !this.state.click2 });
  }

  render() {
    
    return (
      <button 
        id = {this.props.id}
        name = {this.props.name}
        className = {this.props.className}
        onClick = {this.handleClick}  >
        Click Here {this.state.click2 ? '2' : '1'}!
      </button>        
    );
  }
  
}



/**
	*		@desc Component that creates and binds button sub-components
	*		@returns {HTML} Bound buttons
	*/
class BindExample extends React.Component {
  constructor(props) {
    super(props);
    this.handleButtonClick = this.handleButtonClick.bind(this);
    this.clickAction1 = this.clickAction1.bind(this);
    this.clickAction2 = this.clickAction2.bind(this);

    this.state = ({
      //state vars
    });
  }
  
  clickAction1(el) {
    console.log('Action 1 on el: ', el);
  }
  
  clickAction2(el) {
    console.log('Action 2 on el: ', el);
  }
  
  handleButtonClick(el, click2) {

    console.log('click!');
    
    if (click2) this.clickAction2(el);
    else this.clickAction1(el);
  }
  
  render() {
    
    return (
      <div>
        <BoundButton 
          id='ex-btn1' 
          name='bound-button-1'
          className='bound-button'
          click={this.handleButtonClick} />
        <BoundButton 
          id='ex-btn2' 
          name='bound-button-2'
          className='bound-button'
          click={this.handleButtonClick} />
      </div>
    );
  }
  
}

/**
	*		@desc React Class renders full page. Would have more components in a
  *     real app.
	*		@returns {HTML} full app
	*/
class App extends React.Component {
  render() {
    return (
      <div className='pg'>
        <BindExample  />
      </div>
    );
  }
}


/**
	*		Render App to DOM
	*/

/**
	*		@desc ReactDOM renders app to HTML root node
	*		@returns {DOM} full page
	*/
ReactDOM.render(
  <App/>, document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="root">
  <!-- This div's content will be managed by React. -->
</div>

1 Comment

I would like to know why someone would down-vote my answer, which I just spent an hour writing an original solution for, and which works when you run? I think this is a solid answer, and a drive-by down-vote without a comment is quite unhelpful.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.