Shopify / flash-list

A better list for React Native
https://shopify.github.io/flash-list/
MIT License
5.38k stars 277 forks source link

Focus changes when updating data #1337

Open RuudBurger opened 2 weeks ago

RuudBurger commented 2 weeks ago

Current behavior

When refreshing data in Flashlist, focus goes to a different item when using a keyExtractor that returns a unique string id. But when using a keyExtractor with just the index, it works like expected.

Here you'll see when pressing 1 of the items, it jumps from 172->168.

https://github.com/user-attachments/assets/ecea2bf2-38c6-42c1-b82a-eadd8b6cf741

If I use const keyExtractor = useCallback((item) => item.id, []);which uses a stable id, and press a button after scrolling down a bit. The focus is removed from the current TouchableOpacity and put on 1 of the items above that. In my case currentbutton - 4. But if I use const keyExtractor = useCallback((item, index) => index, []); the issue doesn’t happen. But that of course defeats the purpose of the key and reclycling. Adding items in between existing items (which is our usecase but not shown in the code snipit) would cause issues when using index as a key.

Expected behavior

Replacing FlashList with Flatlist fixes the issue. If we add key={item.id} inside the returned element from renderItem it also works with FlashList, but then no recycling will take place.

To Reproduce

A repo kindly setup by Doug can be found here https://github.com/douglowder/flash-list-tv-focus-test

import { FlashList } from '@shopify/flash-list';
import React, { useCallback, useRef, useState } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';

const generateRandomStrings = (length: number) => {
  const randomStrings = [];
  for (let i = 0; i < length; i++) {
    randomStrings.push({
      id: `${i}-${Math.random().toString(36).substring(7)}`,
      content: Math.random().toString(36).substring(7),
    });
  }

  return randomStrings;
};

const initialContent = generateRandomStrings(300);

const Grid = () => {
  const flashListRef = useRef<FlashList<any>>(null);
  const [state, setState] = useState(initialContent);

  const updateStrings = useCallback(() => {
    setState((currentState) => {
      return currentState.map((item) => ({
        id: item.id,
        content: Math.random().toString(36).substring(7),
      }));
    });
  }, []);

  const handleFocus = useCallback((index: number) => {
    console.log(`Grid: scrollToIndex: ${index}`);

    flashListRef?.current?.scrollToIndex({
      index,
      viewPosition: 0.5,
      animated: true,
    });
  }, []);

  const keyExtractor = useCallback((item) => item.id, []);
  // const keyExtractor = useCallback((item, index) => index, []);

  const handleRenderRow = useCallback(
    ({ item, index }: { item; index: number }) => {
      return (
        <TouchableOpacityWrapped
          index={index}
          onFocus={handleFocus}
          onPress={updateStrings}
        >
          <View
            style={{
              height: 50,
              borderWidth: 2,
              margin: 2,
            }}
          >
            <Text>{item.content}</Text>
          </View>
        </TouchableOpacityWrapped>
      );
    },
    [handleFocus, updateStrings],
  );

  return (
    <FlashList
      ref={flashListRef}
      data={state}
      drawDistance={200}
      estimatedItemSize={50}
      keyExtractor={keyExtractor}
      renderItem={handleRenderRow}
      scrollEnabled={false}
      showsHorizontalScrollIndicator={false}
      showsVerticalScrollIndicator={false}
      style={{
        width: 600,
        height: 400,
      }}
    />
  );
};

export default Grid;

const TouchableOpacityWrapped = ({ index, onFocus, ...props }) => {
  const handleFocus = useCallback(() => {
    onFocus(index);
  }, [index, onFocus]);

  return <TouchableOpacity {...props} onFocus={handleFocus} />;
};

Platform:

Android TV (Simulator API 33, but issue also visible on Google 4K with latest Android TV)

Environment

1.7.1

We are currently on 1.6.3, but 1.7.1 also has the issue.

react-native react-native-tvos@0.72.6-1 (I checked with @douglowder and he could also reproduce it on react-native-tvos@0.74.5) react 18.2.0

I've seen #895 but the problem described isn't a speed issue. It happens when no scrolling is done.

douglowder commented 2 weeks ago

I was also able to reproduce this on Apple TV, so the issue does not seem to be platform specific.