1

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);
alexandros84
  • 321
  • 4
  • 14

2 Answers2

5

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

Florent Roques
  • 2,424
  • 2
  • 20
  • 23
  • 1
    Aha! Just what I was looking for. This comes up quite a bit on SO but the questions are worded differently and very seldom cross-linked. The ones that have provided similar answers seem to insist that the wrapper needs to be a class where the project I'm involved in was using const JSX objects and I hadn't been able to find if the two were compatible. Until now. – hippietrail Sep 23 '17 at 03:35
  • 1
    Thank you for your comment! I checked what you said and actually others are right. If you want to avoid re render the event callback you need the wrapper to be a class otherwise it recreates the event callback at every rerender :/ You can check my test here https://snack.expo.io/ByTEKgEsZ. You can wrap the wrapper though x)) as follows to get a const: const ComponentConst = props => ; I've updated my answer to better reflect your observations. Thanks again! – Florent Roques Sep 23 '17 at 16:53
  • Yes indeed when I went home and tried your code I could not get it to work. In fact this question has bad and wrong answers all over SO and the Internet. I lost a couple of days trying to get something working. In the end I had to even use PureComponent for the ListItem class. – hippietrail Sep 24 '17 at 02:05
  • 1
    clean well explained answer. new to messing around with RN and your answer helped a lot, thanks! – Stephen Tetreault Nov 18 '18 at 01:25
-1

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);
hippietrail
  • 15,848
  • 18
  • 99
  • 158
alexandros84
  • 321
  • 4
  • 14
  • @P. Myer Nore : This is precisely what you are commenting in https://stackoverflow.com/questions/2236747/use-of-the-javascript-bind-method. – alexandros84 Aug 17 '17 at 01:55
  • You should not use `bind` or arrow functions in JSX / in the `render` method. They create a new function every time, which results in the component being changed which results in the components being re-rendered every time, which defeats much of the benefit of using React in the first place. This is extremely poorly documented however. – hippietrail Sep 23 '17 at 03:42