This text is about tips on how to implement a visibility sensor for elements in a React Native app. With the assistance of a vertical or horizontal FlatList
and its onViewableItemsChanged
prop, it’s doable to watch occasions every time checklist gadgets seem or disappear within the viewport. Utilizing this idea, you may react to those occasions, e.g., to start out enjoying movies mechanically or to trace “component-seen occasions” for advertising and marketing functions.
On this submit, we’ll talk about tips on how to implement a element visibility sensor in React Native utilizing an instance undertaking and incrementally constructing the answer by way of the next sections:
Every of the “interim options” incorporates a proof for why the answer is incomplete (i.e., attributable to a React Native rendering error, as you’ll see). I needed to clarify the totally different the explanation why issues don’t work; an interaction exists between the restrictions of our FlatList
callback, which have to be secure, and the way these React memoization Hooks (useCallback
, useEffect
, and so on.) work.
Scope of the instance undertaking
The code examples introduced on this article are a part of a GitHub companion undertaking. It’s an Expo undertaking that makes use of TypeScript and is scaffolded with create-expo-app.
The demo use case is deliberately stored easy to focus solely on utilizing the FlatList
API. It reveals Star Wars characters and tracks those that seem on the display screen for a minimum of two seconds. If they seem a number of instances, they are going to be tracked solely as soon as. Thus, the instance app doesn’t characteristic fancy animations on visibility occasions as a result of this may be out of scope.
In distinction, the logic invoked when the FlatList
triggers visibility change occasions is only a monitoring perform to maintain issues easy. That is consistent with monitoring consumer occasions in frequent enterprise functions.
The next screencast showcases the instance app.
A take a look at FlatList
‘s API
Earlier than I dive into the implementation particulars of the instance undertaking, let’s check out the essential FlatList
props to implement an inventory merchandise visibility detector.
In precept, you need to use two FlatList
props:
With viewabilityConfig
, you establish what “viewable” means to your app. The config I used most incessantly is to detect checklist gadgets which might be a minimum of x
% seen for a minimal period of time (y
ms).
viewabilityConfig={{ itemVisiblePercentThreshold: 75, minimumViewTime: 2000, }}
With this instance config, checklist gadgets are thought of seen when they’re a minimum of 75
% within the viewport for a minimum of 2
seconds.
Check out the ViewabilityConfig
a part of ViewabilityHelper
to seek out out in regards to the different legitimate settings, together with their kind definitions.
You want one other FlatList
prop, onViewableItemsChanged
, which is known as every time the visibility of checklist gadgets modifications, in accordance with the settings of our viewabilityConfig
kind definition.
// ... <FlatList information={listItems} renderItem={renderItem} onViewableItemsChanged={information => { // entry information and do sth with viewable gadgets })} viewabilityConfig={{ itemVisiblePercentThreshold: 100, minimumViewTime: 2000, }} // ... /> // ...
Let’s take an in depth take a look at the signature of onViewabilityItemsChanged
, outlined in VirtualizedList
, which is used internally by FlatList
. The information
parameter has the next movement kind definition:
// movement definition a part of VirtualizedList.js onViewableItemsChanged?: ?(information: { viewableItems: Array<ViewToken>, modified: Array<ViewToken>, ... }) => void
A ViewToken
object holds details about the visibility standing of an inventory merchandise.
// movement definition a part of ViewabilityHelper.js kind ViewToken = { merchandise: any, key: string, index: ?quantity, isViewable: boolean, part?: any, ... };
How can we use this? Let’s check out the subsequent TypeScript snippet.
Extra nice articles from LogRocket:
const onViewableItemsChanged = (information: { viewableItems: ViewToken[]; modified: ViewToken[] }): void => { const visibleItems = information.modified.filter((entry) => entry.isViewable); visibleItems.forEach((seen) => console.log(seen.merchandise)); }
On this instance, we’re solely within the modified ViewToken
s that we are able to entry with information.modified
. Right here, we wish to log the checklist gadgets that meet the standards of viewabilityConfig
. As you may see from the ViewToken
definition, the precise checklist merchandise is saved in merchandise
.
What’s the distinction between viewableItems
and modified
?
After onViewableItemsChanged
is invoked by viewabilityConfig
, viewableItems
shops each checklist merchandise that meets the standards of our viewabilityConfig
. Nonetheless, modified
solely holds the delta of the final onViewableItemsChanged
name (i.e., the final iteration).
If you need to do various things for checklist gadgets which might be 100
% seen for 200ms and people which might be 75
% seen for 500ms, you may make the most of FlatList
‘s viewabilityConfigCallbackPairs
prop, which accepts an array of ViewabilityConfigCallbackPair
objects.
That is the movement kind definition of viewabilityConfigCallbackPairs
, which is a part of VirtualizedList
.
// VirtualizedList.js viewabilityConfigCallbackPairs?: Array<ViewabilityConfigCallbackPair>
The movement kind definition of ViewabilityConfigCallbackPair
is a part of ViewabilityHelper
.
// ViewabilityHelper.js export kind ViewabilityConfigCallbackPair = { viewabilityConfig: ViewabilityConfig, onViewableItemsChanged: (information: { viewableItems: Array<ViewToken>, modified: Array<ViewToken>, ... }) => void, ... };
Right here is an instance:
<FlatList information={listItems} renderItem={renderItem} viewabilityConfigCallbackPairs={[ { viewabilityConfig: { minimumViewTime: 200, itemVisiblePercentThreshold: 100, }, onViewableItemsChanged: onViewableItemsChanged100 }, { viewabilityConfig: { minimumViewTime: 500, itemVisiblePercentThreshold: 75 }, onViewableItemsChanged: onViewableItemsChanged75 } ]} // ... />
In case you are within the implementation particulars of the checklist merchandise detection algorithm, you may examine it right here.
FlatList
‘s API distilled (onViewableItemsChanged
, viewabilityConfig
)
With the data we presently have of the related API components, it might appear easy to create a visibility sensor. Nonetheless, the implementation of the perform handed to the onViewableItemsChanged
prop includes working by way of a couple of pitfalls.
Due to this fact, I’ll work out the totally different variations as examples till we get to the ultimate answer. The intermediate options every have bugs attributable to how the FlatList
API is applied and the best way that React works.
We’ll cowl two totally different use circumstances. The primary one might be a easy instance, firing an occasion every time an inventory aspect seems on display screen. The second by way of fourth extra difficult examples construct upon one another to reveal tips on how to hearth occasions when an inventory merchandise is in view, however solely as soon as.
We’re overlaying the second use case as a result of it requires managing state, and that’s the place the ugly issues come up with this FlatList
rendering error.
Listed below are the options we’ll attempt, and what’s unsuitable with every:
- Observe each time a Star Wars character (i.e., checklist aspect) seems on display screen
- Observe each character solely as soon as by introducing state
- Attempt to repair the
useCallback
answer that causes a stale closure challenge (see companion undertaking department stale closure) - Repair the issue through the use of a state updater perform to entry earlier state (see companion undertaking department grasp)
Interim answer 1: Observe each time an inventory aspect seems on display screen
This primary implementation of onViewableItemsChanged
tracks each seen merchandise, every time it seems on the display screen.
const trackItem = (merchandise: StarWarsCharacter) => console.log("### observe " + merchandise.title); const onViewableItemsChanged = (information: { modified: ViewToken[] }): void => { const visibleItems = information.modified.filter((entry) => entry.isViewable); visibleItems.forEach((seen) => { trackItem(seen.merchandise); }); };
We use the modified
object of the information
param handed to the perform. We iterate over this ViewToken
array to retailer solely the checklist gadgets which might be presently seen on the display screen within the visibleItems
variable. Then, we simply name our simplified trackItem
perform to simulate a monitoring name by printing the title of the checklist merchandise to the console.
This could work, proper? Sadly, no. We get a render error.
The implementation of FlatList
doesn’t permit for the perform handed to the onViewableItemsChanged
prop to be recreated throughout the app’s lifecycle.
To resolve this, we’ve got to be sure that the perform doesn’t change after it’s initially created; it must be secure throughout render cycles.
How can we do that? We will use the useCallback
Hook.
const onViewableItemsChanged = useCallback( (information: { modified: ViewToken[] }): void => { const visibleItems = information.modified.filter((entry) => entry.isViewable); visibleItems.forEach((seen) => { trackItem(seen.merchandise); }); }, [] );
With useCallback
in place, our perform is memoized and isn’t recreated as a result of there aren’t any dependencies that may change. The render challenge disappears and monitoring works as anticipated.
Interim answer 2: Observe an inventory aspect solely as soon as by introducing state
Subsequent, we wish to observe every Star Wars character solely as soon as. Due to this fact, we are able to introduce a React state, alreadySeen
, to maintain observe of the characters which have already been seen by the consumer.
As you may see, the useState
Hook shops a SeenItem
array. The perform handed to onViewableItemsChanged
is wrapped right into a useCallback
Hook with one dependency, alreadySeen
. It is because we use this state variable to calculate the subsequent state handed to setAlreadySeen
.
// TypeScript definitions interface StarWarsCharacter { title: string; image: string; } kind SeenItem = { [key: string]: StarWarsCharacter; }; interface ListViewProps { characters: StarWarsCharacter[]; } export perform ListView({ characters, }: ListViewProps) { const [alreadySeen, setAlreadySeen] = useState<SeenItem[]>([]); const onViewableItemsChanged = useCallback( (information: { modified: ViewToken[] }): void => { const visibleItems = information.modified.filter((entry) => entry.isViewable); // carry out facet impact visibleItems.forEach((seen) => { const exists = alreadySeen.discover((prev) => seen.merchandise.title in prev); if (!exists) trackItem(seen.merchandise); }); // calculate new state setAlreadySeen([ ...alreadySeen, ...visibleItems.map((visible) => ({ [visible.item.name]: seen.merchandise, })), ]); }, [alreadySeen] ); // return JSX }
Once more, we’ve got an issue. Due to the dependency alreadySeen
, the perform will get created greater than as soon as and, thus, we’re delighted by our render error once more.
We might eliminate the render error by omitting the dependency with an ESLint ignore
remark.
const onViewableItemsChanged = useCallback( (information: { modified: ViewToken[] }): void => { // ... }, // dangerous repair // eslint-disable-next-line react-hooks/exhaustive-deps [] );
However, as I identified in my article in regards to the useEffect
Hook, you must by no means omit dependencies that you simply use within your Hook. There’s a motive that the ESLint react-hooks plugin tells you that dependencies are lacking.
In our case, we get a stale closure challenge, and our alreadySeen
state variable doesn’t get up to date anymore. The worth stays the preliminary worth, which is an empty array.
But when we do what the ESLint plugin tells us to do, we get our annoying render error once more. We’re at a useless finish.
Someway, we have to discover a answer with an empty dependency array as a result of limitations of the FlatList
implementation.
Interim answer 3: Attempt to repair the stale closure challenge and return to an empty dependency array
How will we get again to an empty dependency array? We will use the state updater perform, which might settle for a perform with the earlier state as parameter. You will discover out extra about state updater capabilities in my LogRocket article on the variations between useState
and useRef
.
const onViewableItemsChanged = useCallback( (information: { modified: ViewToken[] }): void => { const visibleItems = information.modified.filter((entry) => entry.isViewable); setAlreadySeen((prevState: SeenItem[]) => { // carry out facet impact visibleItems.forEach((seen) => { const exists = prevState.discover((prev) => seen.merchandise.title in prev); if (!exists) trackItem(seen.merchandise); }); // calculate new state return [ ...prevState, ...visibleItems.map((visible) => ({ [visible.item.name]: seen.merchandise, })), ]; }); }, [] );
The primary distinction between them is that the state updater perform has entry to the earlier state, and thus, we should not have to entry the state variable alreadySeen
immediately. This fashion, we’ve got an empty dependency array and the perform works as anticipated (see the screencast within the above part on the companion undertaking).
The subsequent part discusses the render error and rancid closure drawback a little bit bit additional.
A detailed take a look at the issue with onViewableItemsChanged
React’s memoization Hooks, comparable to useEffect
and useCallback
, are inbuilt such a means that each element context variable is added to the dependency array. The explanation for that is that Hooks solely get invoked when a minimum of certainly one of these dependencies has been modified with respect to the final run.
To help you with this cumbersome and error-prone activity, the React staff has constructed an ESLint plugin. Nonetheless, even when you already know that the dependency won’t ever change once more throughout runtime, as a very good developer, you need to add it to the dependency array. The plugin authors know that, e.g., state updater capabilities are secure, so the plugin doesn’t demand it within the array, in distinction to the opposite (not pure) capabilities.
In case you return such a state updater perform from a customized Hook and use it in a React element, the plugin mistakenly claims so as to add it as dependency. In such a case, you need to add an ESLint disable
remark to mute the plugin (or reside with the warning).
Nonetheless, that is dangerous observe. Although it shouldn’t be an issue so as to add it to the dependency array, the utilization of the Hook will get extra strong as this variable might change over time, e.g., after a refactoring. If it creates an issue for you, you almost certainly have a bug in your undertaking.
The issue with this onViewableItemsChanged
prop from FlatList
is that you simply can’t add any dependencies to the dependency array in any respect. That’s a limitation of the FlatList
implementation that contradicts with the ideas of memoization Hooks.
Thus, you need to discover a answer to eliminate a dependency. You almost certainly wish to use the strategy as I described above, to make use of a state updater perform that accepts a callback perform to have entry to the earlier state.
If you wish to refactor the implementation of the onViewableItemsChanged
perform — to scale back complexity or to enhance testability by placing it right into a customized Hook — it isn’t doable to forestall a dependency. There’s presently no option to inform React that the customized Hook returns a secure dependency like the results of the inbuilt useRef
Hook or the state updater perform.
In my present undertaking at work, I’ve chosen so as to add an ESLint disable
remark as a result of I do know that the dependency coming from my customized Hook is secure. As well as, you may both ignore the pure capabilities you get because of customized Hooks, or import them from one other file as a result of they don’t use any dependencies themselves.
useCallback( () => { /* Makes use of state updater capabilities from a customized hook or imported pure capabilities. The ESLint plugin doesn't know that the capabilities are secure / don't use any dependencies. It doesn't know that they are often omitted from the array checklist. /* }, // eslint-disable-next-line react-hooks/exhaustive-deps [] )
There have been many discussions prior to now about marking customized Hooks’ return values as secure, however there isn’t a official answer but.
Abstract
FlatList
‘s onViewableItemsChanged
API offers the flexibility to detect elements that seem or disappear on display screen. A FlatList
can be utilized vertically and horizontally. Thus, this can be utilized in most use circumstances, since display screen elements are often organized in an inventory anyway.
The factor with this strategy is that the implementation of viewport detection logic is proscribed, tedious, and error-prone at first. It is because the assigned perform must not ever be recreated after its preliminary creation. Because of this you can’t depend on any dependencies in any respect! In any other case, you‘ll get a render error.
To summarize, listed here are your choices for working round this drawback:
- Wrap the perform you assign to
onViewableItemsChanged
right into auseCallback
Hook with an empty dependency array - In case you use a number of element state variables within your perform, you need to use the state updater perform that accepts a callback with entry to the earlier state, so that you simply eliminate state dependencies
- In case you depend on a state updater perform or a pure perform from one other file (comparable to an imported or a customized Hook), you may preserve the empty dependency array and ignore the ESLint plugin warnings
- In case you depend on some other dependencies (e.g., prop, context, state variable from a customized Hook, and so on.), you need to discover a answer with out utilizing them
If you wish to carry out complicated duties on visibility change occasions, you need to design your occasions in a means that updates your state variables with the state updater perform to maintain an empty dependency array for the useCallback
Hook. Then, you may reply on state modifications, e.g., with an useEffect
Hook, to carry out logic that depends on dependencies. Such a state of affairs might turn out to be difficult, however with the workarounds we’ve mentioned right here, you must have a neater time discovering and implementing an answer that works for you.
LogRocket: Immediately recreate points in your React Native apps.
LogRocket is a React Native monitoring answer that helps you reproduce points immediately, prioritize bugs, and perceive efficiency in your React Native apps.
LogRocket additionally helps you enhance conversion charges and product utilization by exhibiting you precisely how customers are interacting together with your app. LogRocket’s product analytics options floor the the explanation why customers do not full a specific movement or do not undertake a brand new characteristic.
Begin proactively monitoring your React Native apps — attempt LogRocket totally free.