Search code examples
javascriptreactjsreact-nativethisjavascript-objects

ReactNative nested render functions. Question regarding `this` between objects and functions


Here is my confusion:

     constructor(props) {
    super(props);
  }

  hookNav(){
    console.log("work");
  }

  renderItem({ item, index }) {
    return (
      <TouchableOpacity style={{ margin: 9 }} onPress={this.hookNav}>
        <View
          style={{
            flex: 1,
            minWidth: 170,
            maxWidth: 223,
            height: 280,
            maxHeight: 280,
            borderRadius: 10,
          }}
        >
          <ImageBackground
            source={{
              uri: "https://picsum.photos/170/223",
            }}
            style={{ flex: 1 }}
            imageStyle={{ borderRadius: 10 }}
          >
            <Text
              style={{
                color: "white",
                position: "absolute",
                bottom: 20,
                right: 10,
                fontWeight: "bold",
                textShadowColor: "black",
                textShadowOffset: { width: -1, height: 1 },
                textShadowRadius: 10,
              }}
            >
              Topic
            </Text>
          </ImageBackground>
        </View>
      </TouchableOpacity>
    );
  }

  render(){
    return (
      <View style={{ marginBottom: 80, backgroundColor: "white" }}>
        <Header
          centerComponent={{ text: "test", style: { color: "orange" } }}
          rightComponent={{ icon: "add", color: "orange" }}
        />
        <FlatList
          numColumns={2}
          onEndReachedThreshold={0}
          onEndReached={({ distanceFromEnd }) => {
            console.debug("on end reached ", distanceFromEnd);
          }}
          contentContainerStyle={styles.list}
          data={[
            { key: "a" },
            { key: "b" },
            { key: "c" },
            { key: "d" },
            { key: "e" },
            { key: "f" },
          ]}
          renderItem={this.renderItem}
          ListHeaderComponent={
            <Text
              style={{ padding: 10, fontWeight: "bold" }}
            >
              Your Topics
            </Text>
          }
        />
      </View>
    );
  }
}

in this code above, when navigating to this page an error is immediately thrown that this.hookNav is undefined.

However, when I put the hookNav function inside of a state object, like so

constructor(props) {
    super(props);
    state = {
      hookNav : function() {
       console.log("work please");
      }
    }
  }

and in onPress inside of renderItem

const { hookNav} = this.state;
    return (
      <TouchableOpacity style={{ margin: 9 }} onPress={hookNav}>

This works as intended.

It is my understanding that render already has access to the component’s state via this. If renderItem can access the state object, through this, why can it not access this.hookNav() directly? Why does the function need to be encapsulated in an object for this to work?

Thanks.


Solution

  • It's because the value of this inside any Javascript function depends on how that function was called.

    In your non-working example, this.hookNav is inside of the renderItem method. So it will adopt the this of that method - which, as I just said, depends on how it is called. Further inspecting your code shows that the renderItem method isn't called directly by your component but passed as a prop (called renderItem) of the FlatList component. Now I don't know how that component is implemented (I've never used React Native), but almost certainly it calls that function at some point - and when it does, it can't be in the context of your component. This is because FlatList can't possibly know where that function prop is coming from, and when it calls it, it will be treated as an "ordinary" function, not in the context of any particular object. So its this context won't be your component instance, as intended, but will simply be the global object - which doesn't have any hookNav method.

    This problem doesn't happen in your second example because you simply access the function as part of this.state - there is no passing of a method, like the renderItem in the previous example, which depends on an internal this reference. Note that this won't be the case if your hookNav refers to this inside it.

    Such problems with this context are very common in React, but fortunately there are two general solutions which will always fix this:

    1. Bind methods in your component's constructor. If the constructor, in the first example, includes the statement this.renderItem = this.renderItem.bind(this);, then it would work fine. (Note that this is recommended in the official React docs.) It's the "best" solution technically (performance is slightly better than in the other option) - but does involve quite a bit of boilerplate if you have lots of methods which need such binding.

    2. Use arrow functions to define the methods instead. Instead of renderItem({ item, index }) {...}, write it as renderItem = ({ item, index }) => {...}. This works because arrow functions adopt the this of their lexical scope, which will be the class itself - so in other words this will always refer to the component instance, as you almost always want.

    Don't worry if this is confusing - the behaviour of the this keyword in JS is a common stumbling block for beginners to JS, and often enough for those more experienced too. There are a number of good explanations online which demystify it, of which I can especially recommend this one.