ESLINT rule: react/jsx-no-bind
Arrow functions create a new function on every render, decreasing performance as a result of:
- Addtional garbage collection
- Passing changed props to components causing render to travel further into the graph
This is particularly evident when iterating over an array to generate components, which also need function props to handle state change as a result of user interaction, such as onClick/onPress.
Example using Stateless Functional Component:
const List = ({data, onItemPress}) => (
<View>
{data.map(item => (
<View key={item.id} onPress={() => onItemPress(item)}>
<Text>{item.text}</View>
</View>
)}
</View>
)
List.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shapeOf({
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
).isRequired,
onItemPress: PropTypes.func.isRequired,
}
Converting the arrow function to a regular function doesn't solve the problem, you are still creating a new function on every render. Extracting the function requires use of Function.bind
.
const listItemPress = (item, handler) => {
handler(item)
}
const List = ({data, onItemPress}) => (
<View>
{data.map(item => (
<View key={item.id} onPress={listItemPress.bind(null, item, onItemPress)}>
<Text>{item.text}</View>
</View>
)}
</View>
)
List.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shapeOf({
id: PropTypes.number.isRequired
).isRequired,
onItemPress: PropTypes.func.isRequired,
}
This causes the same problem, Function.bind
still creates a new function, bound with the passed arguments.
Eliminating the bind during repetition prevents the passed prop from changing on each render.
class List extends React.PureComponent {
static defaultProps = {
data: PropTypes.arrayOf(
PropTypes.shapeOf({
id: PropTypes.number.isRequired
).isRequired,
onItemPress: PropTypes.func.isRequired,
}
render() {
<View>
{this.props.data.map(item => (
<ListItem key={item.id} onPress={this.props.onItemPress} {...item} />
)}
</View>
}
}
class ListItem extends React.PureComponent {
static propTypes = {
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
}
constructor(props, context) {
super(props, context)
// Bind onPress to the instance, once, here in the constructor
// NB: this is an instance method, do not store it in React state (via setState)
this.onPress = this.onPress.bind(this)
}
onPress() {
// call the passed prop, with item id as the argument as expected
this.props.onPress(this.props.id)
}
render() {
return (
<View onPress={this.onPress}>
<Text>{this.props.text}</Text>
</View>
)
}
}
We can partly simplify the subcomponent implementation by removing the handler class method and dynamically creating a property that is the result of binding.
The downside to this is that its conceivable that the parent component may chose to change the passed prop implementation depending on state, which means we will have to rebind if the props change during the lifecycle of the instance. componentWillReceiveProps
will allow us to handle this case.
class ListItem extends React.PureComponent {
static propTypes = {
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
}
constructor(props, context) {
super(props, context)
this.onPress = this.props.onPress.bind(null, this.props.id)
}
componentWillReceiveProps(newProps) {
this.onPress = this.newProps.onPress.bind(null, this.props.id)
}
render() {
return (
<View onPress={this.onPress}>
<Text>{this.props.text}</Text>
</View>
)
}
}
babel-plugin-transform-class-properties
Class properties are a proposal for ES that can be enabled via the above Babel transform (although the transform may already be included in a plugin/preset that you are using).
If using ESLint, because class properties are a proposal (Stage 2) they are not supported by ESLint core. There is a separate plugin available for eslint or you can use eslint-plugin-babel to transform via Babel before linting.
The class field syntax creates a function which is implicitly bound to this
on each instance, saving the need for a manual bind.
npm install eslint-plugin-babel --save-dev
Add the following to .eslintrc
:
{
"plugins": ["babel"],
"parser": "babel-eslint",
"rules": {
"no-invalid-this": "off",
"babel/no-invalid-this": "error",
}
}
Change the code to use the class fields syntax:
class ListItem extends React.PureComponent {
static propTypes = {
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
}
onPress = () => this.props.onPress(this.props.id)
render() {
return (
<View onPress={this.onPress}>
<Text>{this.props.text}</Text>
</View>
)
}
}