Search code examples
react-nativereact-native-listview

React Native - ListView Scroll To Nested Child Ref?


I am using ListView to display a list of comments and possibly subcomments if they exist on a comment. I'm trying to scroll to a specific subcomment via its ref, but I am not able to get it to work. I used 3 components (boiled down below) to accomplish this:

1. Comments

import React, { Component } from 'react'
import { TouchableOpacity, ListView, View, Text } from 'react-native'
import CommentRow from './commentRow'

const ds = new ListView.DataSource({ rowHasChanged: ( r1, r2 ) => r1.id !== r2.id });
const commentsDataSource = [
  {id: '1', body: 'comment 1'},{id: '2', body: 'comment 2'},{id: '3', body: 'comment 3'},{id: '4', body: 'comment 4'},{id: '5', body: 'comment 5'},{id: '6', body: 'comment 6'},{id: '7', body: 'comment 7'},{id: '8', body: 'comment 8'},{id: '9', body: 'comment 9'},{id: '10', body: 'comment 10'},
  {id: '11', body: 'comment 11'},{id: '12', body: 'comment 12', hasSubComments: true},{id: '13', body: 'comment 13'},{id: '14', body: 'comment 14'},{id: '15', body: 'comment 15'},{id: '16', body: 'comment 16'},{id: '17', body: 'comment 17'},{id: '18', body: 'comment 18'},{id: '19', body: 'comment 19'},{id: '20', body: 'comment 20'}
];

export default class Comments extends Component {
    constructor(props) {
      super(props);

      this.state = {
        dataSource: ds.cloneWithRows(commentsDataSource)
      };
    }

    scrollToSubCommentRef(ref) {
      this.rowz[ref].measure((ox, oy, width, height, px, py) => {
        const offsetY = oy;
        this.refs.mainListView.scrollTo({ y: offsetY })
      });
    }

    render() {
      return (
        <View>
            <TouchableOpacity style={{backgroundColor: 'red', padding: 50}}
                              onPress={() => this.scrollToSubCommentRef('subComment_10')}>
                <Text>Scroll to subComment_10!</Text>
            </TouchableOpacity>

            <ListView ref="mainListView"
                      renderRow={comment => <CommentRow comment={comment} />}
                      dataSource={this.state.dataSource}
                      enableEmptySections={true} />
        </View>
      )
    }
}

2. CommentRow

import React, { Component } from 'react';
import { View } from 'react-native'
import CommentListItem from './commentListItem'

export default class CommentRow extends Component {
    render() {
      const comment = this.props.comment;

      return (
        <View key={`comment_${comment.id}`} style={{overflow: 'hidden'}}>
          <CommentListItem comment={comment} />
        </View>
      )
    }
}

3. CommentListItem

import React, { Component } from 'react'
import { View, Text } from 'react-native'

const subComments = [
  {id: '1', body: 'subcomment 1'},{id: '2', body: 'subcomment 2'},{id: '3', body: 'subcomment 3'},{id: '4', body: 'subcomment 4'},{id: '5', body: 'subcomment 5'},{id: '6', body: 'subcomment 6'},{id: '7', body: 'subcomment 7'},{id: '8', body: 'subcomment 8'},{id: '9', body: 'subcomment 9'},{id: '10', body: 'subcomment 10'},
  {id: '11', body: 'subcomment 11'},{id: '12', body: 'subcomment 12'},{id: '13', body: 'subcomment 13'},{id: '14', body: 'subcomment 14'},{id: '15', body: 'subcomment 15'},{id: '16', body: 'subcomment 16'},{id: '17', body: 'subcomment 17'},{id: '18', body: 'subcomment 18'},{id: '19', body: 'subcomment 19'},{id: '20', body: 'subcomment 20'},
  {id: '21', body: 'subcomment 21'},{id: '22', body: 'subcomment 22'},{id: '23', body: 'subcomment 23'},{id: '24', body: 'subcomment 24'},{id: '25', body: 'subcomment 25'},{id: '26', body: 'subcomment 26'},{id: '27', body: 'subcomment 27'},{id: '28', body: 'subcomment 28'},{id: '29', body: 'subcomment 29'},{id: '30', body: 'subcomment 30'},
  {id: '31', body: 'subcomment 31'},{id: '32', body: 'subcomment 32'},{id: '33', body: 'subcomment 33'},{id: '34', body: 'subcomment 34'},{id: '35', body: 'subcomment 35'},{id: '36', body: 'subcomment 36'},{id: '37', body: 'subcomment 37'},{id: '38', body: 'subcomment 38'},{id: '39', body: 'subcomment 39'},{id: '40', body: 'subcomment 40'},
  {id: '41', body: 'subcomment 41'},{id: '42', body: 'subcomment 42'},{id: '43', body: 'subcomment 43'},{id: '44', body: 'subcomment 44'},{id: '45', body: 'subcomment 45'},{id: '46', body: 'subcomment 46'},{id: '47', body: 'subcomment 47'},{id: '48', body: 'subcomment 48'},{id: '49', body: 'subcomment 49'},{id: '50', body: 'subcomment 50'},
  {id: '51', body: 'subcomment 51'},{id: '52', body: 'subcomment 52'},{id: '53', body: 'subcomment 53'},{id: '54', body: 'subcomment 54'},{id: '55', body: 'subcomment 55'},{id: '56', body: 'subcomment 56'},{id: '57', body: 'subcomment 57'},{id: '58', body: 'subcomment 58'},{id: '59', body: 'subcomment 59'},{id: '60', body: 'subcomment 60'},
  {id: '61', body: 'subcomment 61'},{id: '62', body: 'subcomment 62'},{id: '63', body: 'subcomment 63'},{id: '64', body: 'subcomment 64'},{id: '65', body: 'subcomment 65'},{id: '66', body: 'subcomment 66'},{id: '67', body: 'subcomment 67'},{id: '68', body: 'subcomment 68'},{id: '69', body: 'subcomment 69'},{id: '70', body: 'subcomment 70'}
];

export default class CommentListItem extends Component {
  rowz = []; // to hold subComment refs for scroll access

  subCommentsList = () => {
    return subComments.map((subComment, i) => {
      return (
        <View ref={i => this.rowz["subComment_"+subComment.id] = i} key={"subComment_"+subComment.id}>
          <Text>{subComment.body}</Text>
        </View>
      );
    });
  }

  render() {
    const comment = this.props.comment;

    return (
      <View>
        <Text>{comment.body}</Text>
        {comment.hasSubComments && this.subCommentsList()}
      </View>
    )
  }
}

In the parent component #1 I tried to scroll to a subComment via its ref of subComment_10, but measure gives an undefined error. I understand this.rowz doesn't exist in #1 just in #3 where the subComments map iterates over each subComment and assigns it to the rowz array (I just realized it does not assign the subComment_idhere to the rowz array for some reason).

So how can we fix the ref assignment issue in the #3 map so the rowz array gets a list of all the subComment refs so we can scroll to them? And how can we get the TouchableOpacity with this.scrollToSubCommentRef('subComment_10') in #1 to scroll the mainListView to subComment_10?

UPDATE

With the provided solution, the ref is passed to the rowz array successfully, but as you'll notice, it does not scroll to subComment_10, instead it scrolls to the bottom of comment 10. It should scroll to the top of subComment_10 so that it is the top most visible subComment on click of the TouchableHighlight:

enter image description here


Solution

  • OK, I ran your edited code and figured out what you're missing. The refz array is created locally in CommentListItem class, therefore you can't access it from parent classes. However, since you will be doing all the navigation from parent class, passing a prop array to bottom most level, and filling it there would be a better approach. This way you won't get the this.rowz is undefined error and run your code as expected.

    export default class Comments extends Component {
    constructor(props) {
      super(props);
      this.rowz = []
      this.state = {
        dataSource: ds.cloneWithRows(commentsDataSource)
      };
    }
    
    scrollToSubCommentRef(ref) {
      this.rowz[ref].measure((ox, oy, width, height, px, py) => {
        const offsetY = oy;
        this.refs.mainListView.scrollTo({ y: offsetY })
      });
    }
    
    render() {
      return (
        <View>
            <TouchableOpacity style={{backgroundColor: 'red', padding: 50}}
                              onPress={() => this.scrollToSubCommentRef('subComment_10')}>
                <Text>Scroll to subComment_10!</Text>
            </TouchableOpacity>
    
            <ListView ref="mainListView"
                      renderRow={comment => <CommentRow refArr={this.rowz} comment={comment} />}
                      dataSource={this.state.dataSource}
                      enableEmptySections={true} />
        </View>
      )
    }
    }
    

    Here in Comments class, we pass the array, (this.rowz) that we created in constructor, to CommentsRow class ~

    <CommentRow refArr={this.rowz} comment={comment} />
    

    In CommentRow class, we will just pass what we had from parent class,

    export default class CommentRow extends Component {
        render() {
          const comment = this.props.comment;
    
          return (
            <View key={`comment_${comment.id}`} style={{overflow: 'hidden'}}>
              <CommentListItem refArr={this.props.refArr} comment={comment} />
            </View>
          )
        }
    }
    

    Right here:

    <CommentListItem refArr={this.props.refArr} comment={comment} />
    

    And finally, in CommentListItem class, to fill our array, we can simply call this.props.refArr.push()

    export default class CommentListItem extends Component {
      rowz = []; // to hold subComment refs for scroll access
    
      subCommentsList = () => {
        return subComments.map((subComment, i) => {
          return (
            <View ref={i => this.props.refArr["subComment_"+subComment.id] = i} key={"subComment_"+subComment.id}>
              <Text>{subComment.body}</Text>
            </View>
          );
        });
      }
    
      render() {
        const comment = this.props.comment;
    
        return (
          <View>
            <Text>{comment.body}</Text>
            {comment.hasSubComments && this.subCommentsList()}
          </View>
        )
      }
    }
    

    As you may see clearer here:

    <View ref={i => this.props.refArr["subComment_"+subComment.id] = i} key={"subComment_"+subComment.id}>
    

    It just runs and scrolls smoothly when touchable is pressed. I skipped the import parts in snippets above.