Search code examples
react-nativeanimationreferenceundefinedreact-native-flatlist

FlatList ref scrollToIndex is not a function


I am facing what seems to be a long-lasting issue in react native.
I am using Expo SDK35 with RN version 0.59. I have not updated to Expo SDK36 / RN 0.60 yet, due to large code base, but I could update if that makes up for a solution to my issue.

I have an Animated.View component that has a FlatList child, and I am unable to use the static methods (scrollToIndex() in particular) that should be available on the FlatList reference. See the next example code:

class Example extends React.Component{
    constructor(props){
        super(props);
        this.myRef = null;
    }

    componentDidUpdate = () => {
        /*
            somewhere in code outside this class, a re-render triggers
            and passes new props to this class.
            I do have props change detection, and some more other code,
            but I have removed it in order to minimize the code example here
        */

        // This call throws:
        // TypeError: undefined is not a function (near '...this._scrollRef.scrollTo...')
        this.myRef.scrollToIndex({
            animated: true,
            index: 1,
            viewOffset: 0,
            viewPosition: 0.5
        });

        // Other suggested solution from SO
        // This also throws:
        // TypeError: _this.myRef.getNode is not a function. (In '_this.myRef.getNode()', '_this.myRef.getNode' is undefined)
        this.myRef.getNode().scrollToIndex({
            animated: true,
            index: 1,
            viewOffset: 0,
            viewPosition: 0.5
        });
    }
    render = () => <Animated.View style={{ /* ... some animated props */ }}>
        <FlatList ref={(flatListRef) => { this.myRef = flatListRef; }}
            // more FlatList related props
        />
    </Animated.View>
}

I have tried to use Animated.FlatList instead, still throws the same errors as in the code example above.
I have also tried to use react native's findNodeHandle() utility function on the received flatListRef parameter, but it returns null.

I have found the same issue posted multiple times in the past here on Stack Overflow, most with no answer, or which do not work for me. These posts are also a bit old (a year or so), which is why I am posting again for the same issue.

Did anyone manage to find a solution/workaround for this issue?

EDIT: Possible workaround

As I was playing with code, I tried to use a ScrollView component instead of FlatList - and the scrollTo method works!
The changes were only on the FlatList - ScrollView specific props (so, for a ScrolLView it would be childs instead of data={[...]} and renderItem={()=>{ ... }}, ect.), and the scrollToIndex method in componentDidMount which was replaced by scrollTo.
The render method of the class, with a ScrollView, now looks like this:

    render = () => <Animated.View style={{ /* ... some animated props */ }}>
        <ScrollView ref={(flatListRef) => { this.myRef = flatListRef; }}>
            {/*
                this.renderItem is almost the same as the
                renderItem method used on the FlatList
             */}
             { this.state.dataArray.map(this.renderItem) }
        </ScrollView>
    </Animated.View>

Please note that ScrollView does not have a scrollToIndex() method, so you'll have to cope with manually keeping track of child positions, and maybe, implement a scrollToIndex method of your own.

I am not making this the answer to my question, because the underlying issue remains. But as a workaround, maybe you can go with it and call it a day...


Solution

  • TL;DR;

    
    this.myRef = React.createRef();
    this.myRef.current.doSomething(); // note the use of 'current'
    

    Long version:

    While the idea behind what I was trying was correct, the error in my original post seems to be quite stupid. In my defense, the docs were not clear (probably...). Anyway...

    React.createRef returns an object with a few fields on it, all of them useless for the developer (used by React in the back) - except one: current. This prop holds the current reference to the underlying component that the ref is attached to. The main ref object is not usable for the purpose I meant to in my original question above.

    Instead, this is how I should've used the ref correctly:

    this.myRef.current.scrollToIndex(...)
    

    Hold up, don't crash

    Both the main myRef object, and the current field will be null if the component has not yet mounted, has unmounted at any point later, or if the ref cannot be attached to it for some reason. As you may know (or found out later), null.something will throw an error. So, to avoid it:

    if ((this.myRef !== null) && (this.myRef.current !== null)){
        this.myRef.current.scrollToIndex(...);
    }
    

    Extra insurance

    If you try to call an undefined value as a function on a field on the ref, your code will crash. This can happend if you mistakenly reuse the same ref on multiple components, or if the component you attached it to does not have that method (i.e. View does not have a scrollTo method). To fix this you have two solutions:

    // I find this to be the most elegant solution
    if ((this.myRef !== null) && (this.myRef.current !== null)) {
        if (typeof this.myRef.current.scrollToIndex === "function") {
            this.myRef.current.scrollToIndex(...);
        }
    }
    

    or

    if ((this.myRef !== null) && (this.myRef.current !== null)) {
        if (typeof this.myRef.current.scrollToIndex === "function") {
            try {
                this.myRef.current.scrollToIndex(...);
            } catch (error) {
                console.warn("Something went wrong", error);
            }
        }
    }
    

    I hope this to be useful for anyone else learning to use refs in React. Cheers :)