I have populated a FlatList with data fetched from Google's firebase backend. The implementation is rather standard, here's a stripped down version:
export default class Day extends Component {
state = { data : [], today: false }
componentWillMount = async () => {
const { today } = this.state;
const { calendarDb } = this.props
await calendarDb.onNewAgenda({
day : today
, then: this.parseOnListed
})
}
parseOnListed = blob => {
const { data } = this.state;
data.push(blob)
this.setState({ data: data })
}
renderItem = ({ item }) =>
<Hour data = {item}/>
render = () =>
<FlatList
data = {this.state.data}
renderItem = {this.renderItem}
keyExtractor = {item => item.ID}
/>
}
The issue is that every time a new blob is pushed into data, the <Image/> component in <Hour data={item}/> flickers. This makes the list a no-go in terms of user experience. What gives? <Hour/> is standard as well, and more or less look like this:
const Hour = ({ data }) =>
<View>
<Image source={{uri:data.uri}}/>
<Text> {data.name} </Text>
</View>
The content of <Text> does not flicker, only the image from <Image .../>
Check whether keyExtractor is getting unique ID or not.
The flat list is re-rendering on state update and images are downloaded again. Because, each row is not uniquely identified as said in comments by #Guruparan Giritharan.
I found another reason that triggers this issue, of the FlatList flikering on React native. In my case, it happened every time I updated/changed the state of any function component. So, for instance, I was keeping the fetch results (data) and the next-page-id (for the next paginated fetch) in two separate function components:
const [data, setData] = useState([]);
const [pageId, setPageId] = useState(null);
Hence, every time would capture the results of my fetch, I would first set the data update and then the page id. It was the page id update what was causing the flicker.
const onEndReachedFetch = async () ={
fetch(pageId).then(result => {
setData(result.Data);
setPageId(result.pageId);
});
}
The fix was just to put the state data together so there is a single update instead. Then react is happy and doesn't flicker when adding new items to the list.
const onEndReachedFetch = async () ={
fetch(pageId).then(result => {
setResult(result);
});
}
Beware of any side states that you may be updating in the background, as they may also cause the flickering if they are triggered by anything on the FlatList.
Related
The problem: I have a FlashList that uses React Context to fill in the data (the data is an array of objects that renders a View) but when I update the context and the extraData prop for FlashList, the list does not re-render, or re-renders sometimes, or takes multiple events to actually re-render.
The Code:
// Many imports, they are all fine though
export default () => {
// Relevant context.
const {
cardsArray,
cardsArrayFiltered,
updateCardsArray,
updateCardsArrayFiltered
} = useContext(AppContext);
// Relevant state.
const [didUpdateCards, setDidUpdateCards] = useState(false);
const [cardsFilters, setCardsFilters] = useState([]);
// Relevant refs.
const flatListRef = useRef(null);
// Example effect on mount
useEffect(() => {
setInitialAppState();
}, []);
// Effect that listen to changing on some data that update the context again
useEffect(() => {
const newCardsArray = doSomeFiltering(cardsArray, cardsFilters);
updateCardsArrayFiltered(newCardsArray);
setDidUpdateCards(!didUpdateCards);
}, [cardsFilters]);
// Example of promisey function that sets the initial context.
const setInitialAppState = async () => {
try {
const newCardsArray = await getPromiseyCards();
updateCardsArrayFiltered(newCardsArray);
updateCardsArray(newCardsArray);
} catch ( err ) {
console.debug( err );
}
}
// Renderer for the list item.
const renderListItem = useCallback((list) => <Card key={list.index} card={list.item} />, []);
// List key extractor.
const listKeyExtractor = useCallback((item) => item.id, []);
return (
<FlashList
ref={flatListRef}
data={cardsArrayFiltered}
extraData={didUpdateCards}
keyExtractor={listKeyExtractor}
renderItem={renderListItem}
showsVerticalScrollIndicator={false}
estimatedItemSize={Layout.window.height}
/>
);
}
Notes:
What I did not write all out is the function, logic, view to update cardsFilters however the above effect IS running when it changes.
Moreover, this line here, const newCardsArray = doSomeFiltering(cardsArray, cardsFilters); does indeed return the proper updated data.
What's going on here? I am updating the extraData prop with that didUpdateCards state when the context changes which I thought was the requirement to re-render a FlatList/FlashList.
It looks like object being passed as extraData is a boolean. This means that if the previous value was true, setting it as true again wouldn't count as a change. Instead use an object and update it when you want list to update.
To try just set extraData={{}}. if everything works as expected it means that your update logic has some problem.
I have a hook, which basically renders a Loading Indicator at the right side of my Stack Header.
Here is how it looks like:
export default function useHeaderRightLoadingIndicator(loading = false) {
const navigation = useNavigation();
const renderLoading = useCallback(() => {
if (!loading) return null;
return (
<Loading
size={20}
style={styles.loadingIndicator}
/>
);
}, [loading]);
useEffect(() => {
navigation.setOptions({
headerRight: renderLoading,
});
}, [navigation, renderLoading]);
}
Now, I want to use it during a delete operation. Basically, I have a screen with a lot of erasable items rendered in a FlatList.
The screen contains the stateful array of data... and pass it to the FlatList component.
In order to avoid repeating my self each time I wanna delete the same item (but in a different screen), I have decided to move the responsibility of the deletion to the item itself (I mean, to the FlatList's item). For this, I have been forced to use a context in order to update the graphical interface without having to pass the "setState" node by node.
So... I have the following:
function MyScreen() {
const statefulData = useData(); // consuming context...
return <CustomFlatList data={statefulData} />
}
...
function ErasableItem({ id }) {
const { isLoading, error, deleteItem } = useDeleteItem(id);
return <Text onPress={deleteItem}>Delete!</Text>;
}
Inside the logic of useDeleteItem, I just make an api call and update my context in order to update the UI (delete the item from the list).
This has sense to me... I don't have a super screen which does everything, instead, each node does its own stuff.
But... what if I wanna use the useHeaderRightLoadingIndicator() I described at the beginning? As I have delegated the deletion responsibility to an unmountable component, I will not be able to stop loading it. I mean, if I do:
function ErasableItem({ id }) {
const { isLoading, error, deleteItem } = useDeleteItem(id);
useHeaderRightLoadingIndicator(isLoading);
return <Text onPress={deleteItem}>Delete!</Text>;
}
As the item will be unmounted from the screen, the useEffect of the useHeaderRightLoadingIndicator will not be called the last time, when isLoading toggles from true to false.
In order to fix that, I will have to run a cleanup function, like this:
export default function useHeaderRightLoadingIndicator(loading = false) {
const navigation = useNavigation();
const renderLoading = useCallback(() => {
if (!loading) return null;
return (
<Loading
size={20}
style={styles.loadingIndicator}
/>
);
}, [loading]);
useEffect(() => {
navigation.setOptions({
headerRight: renderLoading,
});
return () => {
// cleanup
navigation.setOptions({ headerRight: null });
};
}, [navigation, renderLoading]);
}
Something that makes me doubt, since if I had delegated the delete operation to the screen, which is not unmountable, I would not have any problem and I would not have to execute that cleanup function.
What is the logic to follow when delegating responsibilities? Is my delegation incorrect in React?
When I change something on my page such as checking radio buttons or switching tabs, new network requests to retrieve images are sent from the browser. I've noticed this with a couple of websites I've made, but there should never be another request; I'm not performing a fetch on changing these values in the frontend. I don't see why the images should be requested again.
I've attached a gif showing it happening in an app I'm making with React Native, although I've seen it in my React projects too. You can see the images flicker as I switch tabs and the network calls in devtools on the right, and I'm also worried about the performance impact.
How can I prevent this from happening?
For context the data flow in my app is as follows:
In App.tsx render MainStackNavigator component.
In MainStackNavigator call firebase to retrieve data (including images). Store that data in Context.
In Home.tsx render the Tabs component, but also create an array containing the tabs data, namely the name of the component to render and the component itself.
In tabs render content based on selected tab.
Home.tsx
export const Home = (): ReactNode => {
const scenes = [
{
key: "first",
component: EnglishBreakfastHome,
},
{
key: "second",
component: SecondRoute,
},
];
return (
<View flex={1}>
<Tabs scenes={scenes} />
</View>
);
};
Tabs.tsx
export const Tabs = ({ scenes }: TabsProps): ReactNode => {
const renderScenes = useMemo(() =>
scenes.reduce((map, scene) => {
map[scene.key] = scene.component;
return map;
}, {})
);
const renderScene = SceneMap(renderScenes);
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Breakfast" },
{ key: "second", title: "Herbal" },
]);
const renderTabBar = ({ navigationState, position }: TabViewProps) => {
const inputRange = navigationState.routes.map((_, i) => i);
return (
<Box flexDirection="row">
{navigationState.routes.map((route, i) => {
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map((inputIndex) =>
inputIndex === i ? 1 : 0.5
),
});
return (
// Tab boxes and styling
);
})}
</Box>
);
};
return (
<TabView // using react-native-tab-view
style={{ flexBasis: 0 }}
navigationState={{ index, routes }}
renderScene={renderScene}
renderTabBar={renderTabBar}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
);
};
Look into something like react-native-fast-image. It is even recommended by the react-native docs.
It handles cashing of images. From the docs:
const YourImage = () => (
<FastImage
style={{ width: 200, height: 200 }}
source={{
uri: 'https://unsplash.it/400/400?image=1',
headers: { Authorization: 'someAuthToken' },
priority: FastImage.priority.normal,
}}
resizeMode={FastImage.resizeMode.contain}
/>
)
The problem here wasn't image caching at all, it was state management.
I made a call to a db to retrieve images, and then stored that array of images in AppContext which wrapped the whole app. Then, everytime I changed state at all, I ran into this problem because the app was being re-rendered. There were two things I did to remove this problem:
Separate Context into separate stores rather than just using a single global state object. I created a ContentContext that contained all the images so that state was kept separate from other state changes and, therefore, re-renders weren't triggered.
I made use of useMemo, and re-wrote the cards (that you can see in the image) to only change when the main images data array changed. This means that, regardless of changes to other state variables, that component will not need to re-render until the images array changes.
Either of the two solutions should work on their own, but I used both just to be safe.
When the component first renders it gets from the switchcase (enters ALL case because its the default value) on the usePositions hook, the value for positions and I set them there, and return them to the Positions controller.
The problem comes when the selectedClient changes from ALL to TODAY (its a context value, I change it in a Sidebar component somewhere else) and before entering the switch case in TODAY value to get the positions of today, I noticed the Positions component already rendered the old state of ALL positions again! Then a second later it renders correctly the todays positions.
I noticed this because my browser on my network tab shows some calls to the server that IndividualPosition makes it means that it rendered
This is my component where it calls the usePositions hook
export const Positions = () => {
const { selectedClient } = useSelectedClientValue();
const { loading, setLoading } = useLoadingValue();
const { positions } = usePositions(selectedClient);
const clientName =
typeof selectedClient === "string" ? selectedClient : selectedClient.name;
useEffect(() => {
setLoading(false);
}, [positions]);
let positionsView = (
<ul className="positions__list">
{positions.map((position) => (
<IndividualPosition position={position} key={position.positionId} />
))}
</ul>
);
return (
<div className="positions" data-testid="positions">
{!loading ? (
<>
<div className="positions__header">
<h2 data-testid="client-name">{clientName}</h2>
</div>
{positionsView}
</>
) : (
<div className="loading-main-window">
<Spinner />
</div>
)}
</div>
);
};
This is the hook where I fetch the data
export const usePositions = selectedClient => {
const [positions, setPositions] = useState([]);
const {setLoading} = useLoadingValue()
useEffect(() => {
setLoading(true);
switch (selectedClient) {
case 'ALL':
getPositions().then(pos => {
setPositions(pos);
});
break;
case 'TODAY':
getTodayPositions().then(pos => {
setPositions(pos);
});
break;
default:
break;
}
}, [selectedClient]);
return {positions, setPositions};
};
The useEffect runs each time the selectedClient change
It looks like the component renders again before getting the todays data and thats why it shows the old state before getting the new data, but I thought that could be avoided with the loading flag
Basically:
-Positions renders, the hook fetches allPositions, its fine
-If I change in the sidebar the value of the selectedClient context value, the Positions components renders again, rendering the IndividualComponent but with the state of allPositions.
- Instead it should wait till todaysPositions fetches to show the new state (loading should do that)
I already tried having a loading local state (my loading is a context value)
Moving the loading in the useEffects on my local component instead of my hook
Setting loading to false inside the hook after fetching the data
Any ideas?
Im new in ReactNative and I'm trying to take some data from here https://www.dystans.org/route.json?stops=Hamburg|Berlin
When I try console.log results it return full API response. I dont know why in first results.distance works and return distance, but when I'm trying to do it inside FlatList nothing is returned. Sometimes it works when i want to return only item.distance but can't somethnig like <Text>{item.stops[0].nearByCities[0].city}</Text> nowhere in my code also in console. Im getting error:
undefined is not an object (evaluating 'results.stops[0]')
imports...
const NewOrContinueScreen = ({ navigation }) => {
const [searchApi, results, errorMessage] = useDystans();
console.log(results.distance);
return (
<SafeAreaView forceInset={{ top: "always" }}>
<Text h3 style={styles.text}>
Distance: {results.distance}
</Text>
<Spacer />
<FlatList
extraData={true}
data={results}
renderItem={({ item }) => (
<Text>{item.distance}</Text>
// <Text>{item.stops[0].nearByCities[0].city}</Text>
)}
keyExtractor={item => item.distance}
/>
<Spacer />
</SafeAreaView>
);
};
const styles = StyleSheet.create({});
export default NewOrContinueScreen;
And here is my hook code:
import { useEffect, useState } from "react";
import dystans from "../api/dystans";
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState("");
const searchApi = async () => {
try {
const response = await dystans.get("route.json?stops=Hamburg|Berlin", {});
setResults(response.data);
} catch (err) {
setErrorMessage("Something went wrong with useDystans");
}
};
useEffect(() => {
searchApi();
}, []);
return [searchApi, results, errorMessage];
};
As the name implies, FlatList is designed to render a list. Your API endpoint returns a JSON Object, not an Array, so there's nothing for the FlatList to iterate. If you want to show all the stops in the list, try passing in the stops list directly.
<FlatList
data={results.stops}
renderItem={({ item }) => (<Text>{item.nearByCities[0].city}</Text>)}
/>
Some side notes: (1) The extraData parameter is used to indicate if the list should re-render when a variable other than data changes. I don't think you need it here at all, but even if you did, passing in true wouldn't have any effect, you need to pass it the name(s) of the variable(s). (2) The keyExtractor parameter is used to key the rendered items from a field inside of them. The stop objects from the API don't have a member called distance so what you had there won't work. From my quick look at the API response, I didn't see any unique IDs for the stops, so you're probably better off letting React key them from the index automatically.