alexandros84
alexandros84

Reputation: 337

React Native-- onPress extract id from "currentTarget"

I have created a game on React and I am trying to adapt my code to React Native. One of the things that is troubling me is how to translate these three lines, since in RN there are no DOM solutions to rely on:

handleClick(e) {

this.props.change(e.currentTarget.id);

}  

What is happening here is that a stateless child is harvesting a clicked elements id (the currentTarget's) and is using it to call with it a method defined inside the parent. This kind of formulation e.currentTarget.id however does not work in RN.

Is there an eloquent way to re-write this one liner in RN?

Note: there are two questions vaguely resembling this one, here and here, however the answers look like more like patches than a structural elegant solution. If you are aware of something pls post an answer.

Edit: It seems that one cannot go around ReactNativeComponentTree.

I have that much so far but this does not work yet:

handlePress(event) {

let number =  ReactNativeComponentTree.getInstanceFromNode(event.currentTarget)._currentElement.id;

this.props.change(number);

}  

Second Edit: Ok maybe I should add a simplistic example of what I am trying to achieve. When I click on any of the flatlist's elements its id should be displayed on the Child's bottom . Clicking on reset will restore default state.

Code of simplistic example below:

    import React, { Component } from 'react';
    import { AppRegistry, FlatList, StyleSheet, Text, View, Button } from 'react-native';
    import ReactNativeComponentTree from 'react-native';

    export default class Parent extends Component {

      constructor(props) {
        super(props);    

        this.state = {  

                quotes: ["a","bnaskdkahhahskkdk","c","d","e","a","b","c","d"],
                    size: [true, true, true, true, true, true, true, true, true],
                    color: [false, false, false, false, false, false, false, false, false],
                    progress: "me"

        };

        this.change = this.change.bind(this);
        this.reset = this.reset.bind(this);

      }

      change(number) {

      this.setState({color: [true, true, true, true, true, true, true, true, true],              progress: number});

      }

      reset() {

        this.setState({color: [false, false, false, false, false, false, false, false, false],
                       progress: "me"
        });

      }

      render() {
        return (
          <View style={styles.container}>
    <Child change={this.change} reset={this.reset} quotes={this.state.quotes} 
           size={this.state.size} color={this.state.color} 
           progress={this.state.progress} />
          </View>
        );
      }
    }

    class Child extends Component {

        constructor(props) {

        super(props);    

        this.handlePress = this.handlePress.bind(this);
        this.handleReset = this.handleReset.bind(this);
      }

        /*handlePress(e) {
          let number = e.currentTarget.id;
            this.props.change(number);
        }*/

        handlePress(event) {

    let number =  ReactNativeComponentTree.getInstanceFromNode(event.currentTarget)._currentElement.id;

    this.props.change(number);

    }  

        handleReset() {
          this.props.reset();
        }

      render() {

        let ar = [];

        for (let i=0; i<this.props.quotes.length; i++) {
          let b = {key: `${i}`, id: i, 
              classSize: this.props.size[i] ? (i%2===0 ? styles.size : styles.oddsize) : "", 
              classColor: this.props.color[i] ? (i%2===0 ? styles.color : styles.oddcolor) : ""}
          ar.push(b);      

        }

        return (
        <View style={styles.container}>
          <Button onPress={this.handleReset} title="Reset" />
            <FlatList
              data={
                ar
              }

    renderItem={({item}) => <Text onPress={this.handlePress} 
    style={[item.classSize, item.classColor]}> {item.id+1} 
    {this.props.quotes[item.id]} </Text> }

            /> 

        <Text style={styles.size}>{this.props.progress}</Text>

        </View>
        );
      }
    }


    const styles = StyleSheet.create({
      container: {
       flex: 1,
       flexDirection: "column",
       //justifyContent: "center",
       alignItems: "center",
       paddingTop: 22,
       //backgroundColor: "purple" 
      },
      size: {
        flex: 1,
        padding: 10,
        fontSize: 18,
        backgroundColor: "grey",
        margin: 1,
        height: 44,
        color: 'gold',
        borderColor: "white",
        borderWidth: "1",
        textAlign: "center"
      },
      oddsize: {
        flex: 1,
        padding: 10,
        fontSize: 18,
        backgroundColor: "white",
        margin: 1,
        height: 44,
        color: 'gold',
        borderColor: "white",
        borderWidth: "1",
        textAlign: "center"
      },
      color: {
        flex: 1,
        padding: 10,
        backgroundColor: 'grey',
        //borderRadius: "25%",
        margin: 1,
        fontSize: 18,
        height: 44,
        color: 'pink',
        borderColor: "red",
        borderWidth: "1"
      },
    oddcolor: {
        flex: 1,
        padding: 10,
        backgroundColor: 'white',
        //borderRadius: "25%",
        margin: 1,
        fontSize: 18,
        height: 44,
        color: 'pink',
        borderColor: "red",
        borderWidth: "1"
      }
    })

    // skip this line if using Create React Native App
    AppRegistry.registerComponent('AwesomeProject', () => Parent);

Upvotes: 1

Views: 5432

Answers (2)

Florent Roques
Florent Roques

Reputation: 2662

A better way (avoiding event callbacks creation at every render)
to get the current pressed element properties (id in this example)
is by wrapping it in a parent component, passing data
and binding all events only once (in constructor)

  1. First declare your component wrapper:
    It needs to be a class and not a stateless functional component otherwise you can't avoid the callback creation at every render

to see the difference, here's a more advanced example in action:
https://snack.expo.io/ByTEKgEsZ (example source code)

class TouchableText extends React.PureComponent {
  constructor(props) {
    super(props);
    this.textPressed = this.textPressed.bind(this);
  }
  
  textPressed(){
    this.props.onPressItem(this.props.id);
  }

  render() {
    return (
      <Text style={styles.item} onPress={this.textPressed}>
        {this.props.children}
      </Text>
    );
  }
}

If you use a const JSX object (stateless functional component), it works but it's not optimal, the event callback will be created every time the component is rendered as the arrow function is actually a shortcut to the render function

const TouchableText = props => {
  const textPressed = () => {
    props.onPressItem(props.id);
  };
  return <Text onPress={textPressed} />;
};
  1. Then use this wrapper instead of your component as following:
class Test extends React.Component {
  constructor(props) {
    super(props);
    //event binding in constructor for performance (happens only once)
    //see facebook advice: 
    //https://facebook.github.io/react/docs/handling-events.html
    this.handlePress = this.handlePress.bind(this);
  }

  handlePress(id) {
    //Do what you want with the id
  }

  render() {
    return (
      <View>
        <FlatList
          data={ar}
          renderItem={({ item }) => (
            <TouchableText
              id={item.id}
              onPressItem={this.handlePress}
            >
              {`Component with id ${item.id}`}
            </TouchableText>
          )}
        />
      </View>
    );
  }
}

As written in React Native Handling Events Doc, there's another possible syntax to avoid binding in the constructor (experimental though: i.e. which may or may not be supported in the future!):

If calling bind annoys you, there are two ways you can get around this. If you are using the experimental property initializer syntax, you can use property initializers to correctly bind callbacks

this means we could only write

handlePress = (id) => {
    //`this` is already bound!
}

instead of

constructor(props) {
    super(props);
    //manually bind `this` in the constructor
    this.handlePress = this.handlePress.bind(this);
}
handlePress(id) {

}

Some references:
Event handlers and Functional Stateless Components
React Binding Patterns: 5 Approaches for Handling this

Upvotes: 5

alexandros84
alexandros84

Reputation: 337

After 8 hours of searching I ve found the solution on my own, thanks to the fantastic calculator tutorial of Kyle Banks.

https://kylewbanks.com/blog/react-native-tutorial-part-3-developing-a-calculator

Well basically the solution is in binding item.id along this in the assignment of the onPress event listener, like so:

renderItem={({item}) => <Text
  onPress={this.handlePress.bind(this, item.id)} 
  style={[item.classSize, item.classColor]}>
  {item.id+1} 
  {this.props.quotes[item.id]}
</Text> }

After doing so, the only thing you have to do is to define handlePress like so:

handlePress(e) {
    this.props.change(e);
}

Where e is the item.id, on the Child and change() on the Parent:

  change(number) {

  this.setState({color: [true, true, true, true, true, true, true, true, true], progress: number});

  }

The complete code works as intended.

import React, { Component } from 'react';
import { AppRegistry, FlatList, StyleSheet, Text, View, Button } from 'react-native';
import ReactNativeComponentTree from 'react-native';

export default class Parent extends Component {

  constructor(props) {
    super(props);    

    this.state = {  

            quotes: ["a","bnaskdkahhahskkdk","c","d","e","a","b","c","d"],
                size: [true, true, true, true, true, true, true, true, true],
                color: [false, false, false, false, false, false, false, false, false],
                progress: "me"

    };

    this.change = this.change.bind(this);
    this.reset = this.reset.bind(this);

  }

  change(number) {

  this.setState({color: [true, true, true, true, true, true, true, true, true], progress: number});

  }

  reset() {

    this.setState({color: [false, false, false, false, false, false, false, false, false],
                   progress: "me"
    });

  }

  render() {
    return (
      <View style={styles.container}>
<Child change={this.change} reset={this.reset} quotes={this.state.quotes} 
       size={this.state.size} color={this.state.color} 
       progress={this.state.progress} />
      </View>
    );
  }
}

class Child extends Component {

    constructor(props) {

    super(props);    

    this.handlePress = this.handlePress.bind(this);
    this.handleReset = this.handleReset.bind(this);
  }

handlePress(e) {
        this.props.change(e);
    }

    /*  handlePress(event) {

let number =  ReactNativeComponentTree.getInstanceFromNode(event.currentTarget)._currentElement.id;

this.props.change(number);

}  */

    handleReset() {
      this.props.reset();
    }

  render() {

    let ar = [];

    for (let i=0; i<this.props.quotes.length; i++) {
      let b = {key: `${i}`, id: i, 
          classSize: this.props.size[i] ? (i%2===0 ? styles.size : styles.oddsize) : "", 
          classColor: this.props.color[i] ? (i%2===0 ? styles.color : styles.oddcolor) : ""}
      ar.push(b);      

    }

    return (
    <View style={styles.container}>
      <Button onPress={this.handleReset} title="Reset" />
        <FlatList
          data={
            ar
          }

renderItem={({item}) => <Text onPress={this.handlePress.bind(this, item.id)} 
style={[item.classSize, item.classColor]}> {item.id+1} 
{this.props.quotes[item.id]} </Text> }

        /> 

    <Text style={styles.size}>{this.props.progress}</Text>

    </View>
    );
  }
}


const styles = StyleSheet.create({
  container: {
   flex: 1,
   flexDirection: "column",
   //justifyContent: "center",
   alignItems: "center",
   paddingTop: 22,
   //backgroundColor: "purple" 
  },
  size: {
    flex: 1,
    padding: 10,
    fontSize: 18,
    backgroundColor: "grey",
    margin: 1,
    height: 44,
    color: 'gold',
    borderColor: "white",
    borderWidth: "1",
    textAlign: "center"
  },
  oddsize: {
    flex: 1,
    padding: 10,
    fontSize: 18,
    backgroundColor: "white",
    margin: 1,
    height: 44,
    color: 'gold',
    borderColor: "white",
    borderWidth: "1",
    textAlign: "center"
  },
  color: {
    flex: 1,
    padding: 10,
    backgroundColor: 'grey',
    //borderRadius: "25%",
    margin: 1,
    fontSize: 18,
    height: 44,
    color: 'pink',
    borderColor: "red",
    borderWidth: "1"
  },
oddcolor: {
    flex: 1,
    padding: 10,
    backgroundColor: 'white',
    //borderRadius: "25%",
    margin: 1,
    fontSize: 18,
    height: 44,
    color: 'pink',
    borderColor: "red",
    borderWidth: "1"
  }
})

// skip this line if using Create React Native App
AppRegistry.registerComponent('AwesomeProject', () => Parent);

Upvotes: -1

Related Questions