Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Focus changes when updating data #1337

Open
RuudBurger opened this issue Aug 28, 2024 · 1 comment
Open

Focus changes when updating data #1337

RuudBurger opened this issue Aug 28, 2024 · 1 comment
Labels
bug Something isn't working

Comments

@RuudBurger
Copy link

RuudBurger commented Aug 28, 2024

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.

Untitled.mov

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 [email protected] (I checked with @douglowder and he could also reproduce it on [email protected])
react 18.2.0

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

@RuudBurger RuudBurger added the bug Something isn't working label Aug 28, 2024
@douglowder
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants