Bug Description
1. Summary
App Name: DirecTV
Bug Severity
Select one that applies
- Impacts operation of app
- Blocks current development
- Improvement suggestion
- Issue with documentation (If selected, please share the doc link and describe the issue)
- Other
2. Steps to Reproduce
- Create a sample app
- Intall a
@amzn/shopify__flash-listpkg. - Use provided js code below (see 7) as source code in the src/App.tsx
- Run the app
- Move focus to item 4, 5, 6
- Wait data to be updated
- Observe the focus was lost
3. Observed Behavior
Focus is lost in the sample app (in DTV app it land on prev. item)
4. Expected Behavior
The same as on FireTV/AndroidTV/tvOS platforms: Once the data has been updated, the focus should stay on the same item on the list.
4.a Possible Root Cause & Temporary Workaround
- Make
drawDistancebigger so list will render all items - Use
keyExtractor= (_,index) => index
^^^ no acceptable in our case (itโs a pref. degradation)
5. Logs or crash report
(NOBRIDGE) LOG Focused on {"index": 3} <-- I move focus to 4 item
(NOBRIDGE) LOG Item UN_Mounted {"index": 0} <-- 1st item was unmounted
(NOBRIDGE) LOG Item Mounted {"index": 8}
(NOBRIDGE) LOG Updating data <-- Data has beed updated
(NOBRIDGE) LOG Render App
(NOBRIDGE) LOG Item Blur {"index": 3} <-- Focus lost :(
6. Environment
Please fill out the fields related to your bug below:
-
SDK Version:
0.20.3719 -
App State:
Foreground -
OS Information
cat: /etc/os-release: No such file or directory
7. Example Code Snippet / Screenshots / Screengrabs
import React, {
useState,
useEffect,
useCallback,
useRef,
ComponentRef,
} from 'react';
import {
StyleSheet,
ImageBackground,
View,
Text,
TouchableOpacity,
} from 'react-native';
import {FlashList} from '@amzn/shopify__flash-list';
const DATA = new Array(100).fill(0).map((_, i) => ({
id: Math.random().toString(36).substr(2, 2),
title: `Item ${i + 1}`,
}));
const ITEM_WIDTH = 400;
function MyItem({item, index, onFocus}) {
const [focused, setFocused] = useState(false);
const styles = getStyles();
const myOnFocus = useCallback(
(e) => {
console.log('Item Focus', {index});
setFocused(true);
onFocus?.(e);
},
[onFocus],
);
const onBlur = useCallback(() => {
console.log('Item Blur', {index});
setFocused(false);
}, []);
return (
<TouchableOpacity
key={item.id}
hasTVPreferredFocus={index === 0}
onFocus={myOnFocus}
onBlur={onBlur}
style={[styles.item, focused && styles.focus]}>
<MountLogger name="Item" index={index} />
<View>
<Text style={{fontSize: 40}}>{item.title}</Text>
</View>
</TouchableOpacity>
);
}
const viewabilityConfig = {
itemVisiblePercentThreshold: 10,
minimumViewTime: 100,
};
export const App = () => {
const [data, setData] = useState(DATA);
const ref = useRef<ComponentRef<typeof FlashList> | null>(null);
const styles = getStyles();
useEffect(() => {
const id = setInterval(() => {
console.log('Updating data');
setData([...DATA].map(item => ({...item})));
}, 10_000);
return () => clearInterval(id);
}, []);
const keyExtractor = useCallback((item) => item.id, []);
const renderItem = useCallback(
({item, index}) => (
<MyItem
item={item}
index={index}
onFocus={() => {
console.log('Focused on', {index});
ref.current?.scrollToIndex({
index,
animated: true,
viewPosition: 0.01,
});
}}
/>
),
[],
);
console.log('Render App');
return (
<ImageBackground
source={require('./assets/background.png')}
style={styles.background}>
<FlashList
ref={ref}
data={data}
extraData={data}
horizontal={true}
renderItem={renderItem}
keyExtractor={keyExtractor}
drawDistance={ITEM_WIDTH}
scrollEnabled={false}
estimatedItemSize={ITEM_WIDTH}
removeClippedSubviews={false}
viewabilityConfig={viewabilityConfig}
style={styles.list}
/>
</ImageBackground>
);
};
const getStyles = () =>
StyleSheet.create({
background: {
flex: 1,
},
list: {
height: 400,
},
focus: {borderColor: 'red', borderWidth: 5, opacity: 1},
item: {
width: ITEM_WIDTH - 20,
height: 180,
margin: 10,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.3,
shadowRadius: 3,
elevation: 5,
},
});
function MountLogger({name, ...props}) {
useEffect(() => {
console.log(name + ' Mounted', props);
return () => console.log(name + ' UN_Mounted', props);
}, []);
return null;
}