Friday, August 26, 2022
HomeWeb DevelopmentImplementing a element visibility sensor with React Native

Implementing a element visibility sensor with React Native


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.

Our example app with a visibility sensor

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 ViewTokens 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:

  1. Observe each time a Star Wars character (i.e., checklist aspect) seems on display screen
  2. Observe each character solely as soon as by introducing state
  3. Attempt to repair the useCallback answer that causes a stale closure challenge (see companion undertaking department stale closure)
  4. 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.

Render error whenever the function is created multiple times

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.

Track a character every time they appear on screen

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.

The initial value of the array does not get updated

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 a useCallback 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 — .

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments