React.js: the most efficient way to pass a parameter to an event handler without bind() in a component
I have explained it in my another post: click event in react component.
Never use inline arrow function if you're worried about its performance. You can still use the public class method and bind the context this
.
handleClick = () => () => {
this.setState({ // this works now
isClicked:true
});
}
You can pass any parameters you like just like this:
handleClick = (param1, param2, param3) => (event) => {
As per devserkan's comment,
This is currying and same as other options. This function is also recreated in every render.
No. It doesn't. See the note from docs:
If this callback is passed as a prop to lower components, those components might do an extra re-rendering. We generally recommend binding in the constructor or using the class fields syntax, to avoid this sort of performance problem.
Also, see the comment from bigga-hd below the certainperformance's answer:
Avoid declaring arrow functions or binding in render for optimal performance. Declare your functions outside of render. No more function allocations on each render.
How do you call this handler?
You can call the method just like this:
onClick={this.handleClick(param1,param2,param3)}
PS: I did not mark this post as duplicate as question scope is significantly different. So, just linked the post to get you dig into more detail.
Instead of .bind
ing or creating an anonymous arrow function in render()
, you can create the bound/anonymous function outside of render()
, such as on the instantiated object, in the constructor, or something like that, and use a reference to that singular (never re-created) function. For example, run once:
this.boundHandleClick = this.handleClick.bind(this, 111);
or
this.boundHandleClick = () => this.handleClick(111);
Then, in render
, reference boundHandleClick
:
return (
<div className="App">
<button onClick={this.boundHandleClick}>Click</button>
</div>
);
If you need to use the parameters (111
) inside of render
, then you could use object lookup to see if a bound function with that parameter exists yet. If it does, just use that bound function - else, create it (once, so it won't have to be created again whenever you use that parameter in the future):
this.boundClicks = {};
// ...
if (!this.boundClicks['111']) this.boundClicks['111'] = () => this.handleClick(111);
return (
<div className="App">
<button onClick={this.boundClicks['111']}>Click</button>
</div>
);
This depends on how you get the parameter. There will be times you can't avoid using a .bind
or an arrow function easily but most of the times you can get the parameter somehow. As you can see in @CertainPerformance's answer if you can use this argument in the constructor, you can prefer this way. But there can be other approaches.
For example, assume that you have a list in the state. Instead of mapping this list directly and using a .bind
or an arrow function there, you can pass the list elements to a child component and then use a callback handler there.
class App extends React.Component {
state = {
list: [ "foo", "bar" ],
};
handleClick(el) { console.log( el ) }
render() {
return (
<div>
{this.state.list.map( el => (
<Child key={el} el={el} onClick={this.handleClick} />
) )}
</div>
);
}
}
const Child = ( props ) => {
const handleClick = () => props.onClick( props.el );
return (
<div>
{props.el}
<button onClick={handleClick}>Click</button>
</div>
);
};
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"></div>
I am updating my answer with a demonstration how using an inline arrow function, or binding it, or using a curried function causes a recreation here.
Assume that you have a component and this component has a child component written as React.PureComponent
. Normally, if this child's props does not change it won't re-render. Cool. We have a class method in our parent component and want to pass this as a handler to our child component. Let's see what is going on here.
First, I don't pass the handler and when you increment the counter in the parent, child component does not rerender again (except the initial render). This is because we defined it as a PureComponent
. We don't want it to be rerendered unless its props changes.
class App extends React.Component {
state = {
counter: 0,
};
increment = () =>
this.setState( currentState => ( {
counter: currentState.counter + 1,
} ) );
handleClick(param) { console.log( param ) }
render() {
return (
<div>
<button onClick={this.increment}>Increment</button>
Counter is: {this.state.counter}
<Child />
</div>
);
}
}
class Child extends React.PureComponent {
render() {
console.log( "child rendered" );
return (
<div>
<button onClick={this.props.onClick}>Click</button>
</div>
);
}
}
ReactDOM.render( <App />, document.getElementById( "root" ) );
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
As you can see child component isn't rerendered. Now lets do this with our class method, using an inline arrow function.
class App extends React.Component {
state = {
counter: 0,
};
increment = () =>
this.setState( currentState => ( {
counter: currentState.counter + 1,
} ) );
handleClick( param ) { console.log( param ) }
render() {
return (
<div>
<button onClick={this.increment}>Increment</button>
Counter is: {this.state.counter}
<Child onClick={() => this.handleClick( "some param" )} />
</div>
);
}
}
class Child extends React.PureComponent {
render() {
console.log( "child rendered" );
return (
<div>
<button onClick={this.props.onClick}>Click</button>
</div>
);
}
}
ReactDOM.render( <App />, document.getElementById( "root" ) );
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Ooops, child is rendered when we increment the counter. But, it does not have any relationship to the counter state, we don't want this. So why is it rerendering? This is because we are using an inline arrow function in the onClick
prop it is getting. Since this function is recreated in every render of the parent, its reference changes to a different function and child thinks that it gets a new prop! But in reality it does not get it. We can use the parameter with our handler but there is unnecessary rendering.
Now with the .bind
. I don't use this
in the bind since we don't use this
in our simple method. It just logs a parameter.
class App extends React.Component {
state = {
counter: 0,
};
increment = () =>
this.setState( currentState => ( {
counter: currentState.counter + 1,
} ) );
handleClick( param ) { console.log( param ) }
render() {
return (
<div>
<button onClick={this.increment}>Increment</button>
Counter is: {this.state.counter}
<Child onClick={this.handleClick.bind( null, "some param" )} />
</div>
);
}
}
class Child extends React.PureComponent {
render() {
console.log( "child rendered" );
return (
<div>
<button onClick={this.props.onClick}>Click</button>
</div>
);
}
}
ReactDOM.render( <App />, document.getElementById( "root" ) );
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Same here, we can use the parameter but there is unnecessary rendering. Now with a curried function.
class App extends React.Component {
state = {
counter: 0,
};
increment = () =>
this.setState( currentState => ( {
counter: currentState.counter + 1,
} ) );
handleClick( param ) {
return function() {
console.log( param )
}
}
render() {
return (
<div>
<button onClick={this.increment}>Increment</button>
Counter is: {this.state.counter}
<Child onClick={this.handleClick( "some param" )} />
</div>
);
}
}
class Child extends React.PureComponent {
render() {
console.log( "child rendered" );
return (
<div>
<button onClick={this.props.onClick}>Click</button>
</div>
);
}
}
ReactDOM.render( <App />, document.getElementById( "root" ) );
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Surprise! Again unnecessary rendering. Now, for one component this is not so important. But what if your app has hundereds of components like this child?
Now, lets assume I'm getting the param somehow. I am mimicking it with a hardcoded string here.
class App extends React.Component {
state = {
counter: 0,
};
increment = () =>
this.setState( currentState => ( {
counter: currentState.counter + 1,
} ) );
handleClick() { console.log( "some param" ) }
render() {
return (
<div>
<button onClick={this.increment}>Increment</button>
Counter is: {this.state.counter}
<Child onClick={this.handleClick} />
</div>
);
}
}
class Child extends React.PureComponent {
render() {
console.log( "child rendered" );
return (
<div>
<button onClick={this.props.onClick}>Click</button>
</div>
);
}
}
ReactDOM.render( <App />, document.getElementById( "root" ) );
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
Duh! No unnecessary rerendering as expected since we used the function reference. I can use the param but life is not so easy and OP actually asking how can we use the parameter without using an inline arrow function, or binding or with a curried function. All the fuss is about this.
Even though we don't pass this handler to a component down, it is still recreated in every render of the parent as we see here. If you have a list of items, lets say 500 of them, and you are mapping them into the buttons in the parent component and use an arrow function, etc here, this means they will be recreated (500 times) in every render!
So, there isn't any easy way of doing this. If our parameter is not coming from the event object then either we use @CertainPerformance's solution or try to change our logic like I do here.