Scrolling issues with FlatList when rows are variable height

react-native, react-native-flatlist

Solution

Use scrollToOffset() instead:

    export default class List extends React.PureComponent {

        // Gets the total height of the elements that come before
        // element with passed index
        getOffsetByIndex(index) {
            let offset = 0;
            for (let i = 0; i < index; i += 1) {
                const elementLayout = this._layouts[i];
                if (elementLayout && elementLayout.height) {
                    offset += this._layouts[i].height;
                }
            }
            return offset;
        }

        // Gets the comment object and if it is a comment
        // is in the list, then scrolls to it
        scrollToComment(comment) {
            const { list } = this.props;
            const commentIndex = list.findIndex(({ id }) => id === comment.id);
            if (commentIndex !== -1) {
                const offset = this.getOffsetByIndex(commentIndex);
                this._flatList.current.scrollToOffset({ offset, animated: true });
            }
        }

        // Fill the list of objects with element sizes
        addToLayoutsMap(layout, index) {
            this._layouts[index] = layout;
        }

        render() {
            const { list } = this.props;

            return (
                <FlatList
                    data={list}
                    keyExtractor={item => item.id}
                    renderItem={({ item, index }) => {
                        return (
                            <View
                                onLayout={({ nativeEvent: { layout } }) => {
                                    this.addToLayoutsMap(layout, index);
                                }}
                            >
                                <Comment id={item.id} />
                            </View>
                        );
                    }}
                    ref={this._flatList}
                />
            );
        }
    }

- When rendering, I get the size of each element of the list and write it into an array:

`onLayout={({ nativeEvent: { layout } }) => this._layouts[index] = layout}`

When it is necessary to scroll the screen to the element, I summarize the heights of all the elements in front of it and get the amount to which to scroll the screen (getOffsetByIndex method).

I use the scrollToOffset method:

`this._flatList.current.scrollToOffset({ offset, animated: true });`

(this._flatList is ref of FlatList)

Problem

I'm using a `FlatList` where each row can be of different height (and may contain a mix of both text and zero or more images from a remote server). I cannot use `getItemLayout` because I don't know the height of each row (nor the previous ones) to be able to calculate. The problem I'm facing is that I cannot scroll to the end of the list (it jumps back few rows when I try) and I'm having issues when trying to use `scrollToIndex` (I'm guessing due to the fact I'm missing `getItemLayout`). I wrote a sample project to demonstrate the problem: ``` import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Image, FlatList } from 'react-native'; import autobind from 'autobind-decorator'; const items = count => [...Array(count)].map((v, i) => ({ key: i, index: i, image: 'https://dummyimage.com/600x' + (((i % 4) + 1) * 50) + '/000/fff', })); class RemoteImage extends Component { constructor(props) { super(props); this.state = { style: { flex: 1, height: 0 }, }; } componentDidMount() { Image.getSize(this.props.src, (width, height) => { this.image = { width, height }; this.onLayout(); }); } @autobind onLayout(event) { if (event) { this.layout = { width: event.nativeEvent.layout.width, height: event.nativeEvent.layout.height, }; } if (!this.layout || !this.image || !this.image.width) return; this.setState({ style: { flex: 1, height: Math.min(this.image.height, Math.floor(this.layout.width * this.image.height / this.image.width)), }, }); } render() { return ( <Image onLayout={this.onLayout} source={{ uri: this.props.src }} style={this.state.style} resizeMode='contain' /> ); } } class Row extends Component { @autobind onLayout({ nativeEvent }) { let { index, item, onItemLayout } = this.props; let height = Math.max(nativeEvent.layout.height, item.height || 0); if (height != item.height) onItemLayout(index, { height }); } render() { let { index, image } = this.props.item; return ( <View style={[styles.row, this.props.style]}> <Text>Header {index}</Text> <RemoteImage src = { image } /> <Text>Footer {index}</Text> </View> ); } } export default class FlatListTest extends Component { constructor(props) { super(props); this.state = { items: items(50) }; } @autobind renderItem({ item, index }) { return <Row item={item} style={index&1 && styles.row_alternate || null} onItemLayout={this.onItemLayout} />; } @autobind onItemLayout(index, props) { let items = [...this.state.items]; let item = { ...items[index], ...props }; items[index] = { ...item, key: [item.height, item.index].join('_') }; this.setState({ items }); } render() { return ( <FlatList ref={ref => this.list = ref} data={this.state.items} renderItem={this.renderItem} /> ); } } const styles = StyleSheet.create({ row: { padding: 5, }, row_alternate: { backgroundColor: '#bbbbbb', }, }); AppRegistry.registerComponent('FlatListTest', () => FlatListTest); ```

Original source