npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-observable-list

v4.0.1

Published

It provides item tracking functionality for lists that offer an `onViewableItemsChanged` callback with the type `({viewableItems: ViewToken[]}) => void` and a `renderItem` property with the type `({item: ItemT, index: number}) => React.ReactElement`.

Readme

react-native-observable-list

English | 한국어

This library was developed as a replacement for the browser's IntersectionObserver API.

It provides item tracking functionality for lists that offer an onViewableItemsChanged callback with the type ({viewableItems: ViewToken[]}) => void and a renderItem property with the type ({item: ItemT, index: number}) => React.ReactElement.

demo1 (useInViewPort, Example 1-3)

demo1

demo2 (useIsFirst, Example 4)

demo2

Installation

npm install react-native-observable-list

Usage

You can use the useInViewPort hook anywhere within the item to register an event handler that will trigger when the item enters or leaves the viewport.

The optional deps array of useInViewPort and useIsFirst does not work like useEffect dependencies. When deps change, the callback is not executed but re-registered: the registered callback is simply replaced with a new one that captures the latest values, and execution is still driven by viewport enter/leave events.

The newly created component with observe will have an observeEnabled (optional, default is true) property.

$$enabled is deprecated. It still works as a fallback when observeEnabled is not set, but it will be removed in a future major release.

import { FlatList } from 'react-native';
import { observe, useInViewPort } from 'react-native-observable-list';
import { useIsFocused } from '@react-navigation/native';

const ObservableFlatList = observe(FlatList);

const data = Array.from({ length: 100 });

const Item = ({ id }) => {
  // The callback is re-registered whenever deps change.
  // clean is executed when an item leaves the viewport. (not unmount)
  useInViewPort(() => {
    console.log(`id: ${id} is visible.`);

    return () => {
      console.log(`id: ${id} has been hidden.`);
    };
  }, []);
  return <View style={{ height: 100 }} />;
};

const App = () => {
  const isFocused = useIsFocused();
  return (
    <ObservableFlatList
      observeEnabled={isFocused} // if you need
      data={data}
      renderItem={() => <Item />}
    />
  );
};

Observer

Observer is a component wrapper around useInViewPort for places where you cannot call the hook directly. It renders its single child as-is (enforced with React.Children.only) and takes two callbacks.

  • onInViewPort: () => T (required): runs when the item enters the viewport.
  • onOutOfViewPort?: (value: T) => void (optional): runs when the item leaves the viewport, receiving the value returned by onInViewPort in the same enter/leave cycle. The generic T links the two signatures — e.g. with onInViewPort={() => Date.now()}, the argument of onOutOfViewPort is inferred as number.
import { FlatList, View } from 'react-native';
import { observe, Observer } from 'react-native-observable-list';

const ObservableFlatList = observe(FlatList);

const data = Array.from({ length: 100 }, (_, i) => i);

const App = () => {
  return (
    <ObservableFlatList
      data={data}
      renderItem={({ item }) => (
        <Observer
          onInViewPort={() => Date.now()} // runs on enter
          onOutOfViewPort={(enteredAt) => {
            // runs on leave with the value returned on enter
            console.log(`${item} was visible for ${Date.now() - enteredAt}ms`);
          }}
        >
          <View style={{ height: 100 }} />
        </Observer>
      )}
    />
  );
};

Internally, Observer keeps both callbacks in refs and registers a single stable wrapper with useInViewPort(wrapper, []). Because deps is fixed to [], changing the callback prop identities on re-render never re-registers (replaces) the callback — execution timing is decided by viewport enter/leave events only, and the latest callbacks are always the ones executed. The value returned on enter is captured per cycle and handed to the latest onOutOfViewPort on leave.

So while the deps of useInViewPort work differently from useEffect (replace on change, never execute), Observer goes one step further: there are no deps to manage at all.

IsFirst

IsFirst is a component wrapper around useIsFirst for places where you cannot call the hook directly. It renders its single child as-is (enforced with React.Children.only) and takes one callback.

  • onFirst: () => void (required): runs when the item becomes the first visible item.
import { FlatList, View } from 'react-native';
import { observe, IsFirst } from 'react-native-observable-list';

const ObservableFlatList = observe(FlatList);

const data = ['blue', 'yellow', 'green', 'orange'];

const App = () => {
  return (
    <ObservableFlatList
      data={data}
      renderItem={({ item: color }) => (
        <IsFirst
          onFirst={() => {
            console.log(`${color} is now the first visible item.`);
          }}
        >
          <View style={{ backgroundColor: color, height: 500 }} />
        </IsFirst>
      )}
    />
  );
};

Internally, IsFirst keeps the callback in a ref and registers a single stable wrapper with useIsFirst(wrapper, []), the same structure as Observer: changing the callback prop identity on re-render never re-registers anything, and the latest callback is always the one executed on first-item transitions.

Examples

install dependencies

yarn install

run on expo android

yarn example android

run on expo ios

yarn example ios

Example1 (Nesting)

It supports both reverse and forward nesting.

Example2 (react-native-reanimated-carousel)

It can also be used with containers that do not have the onViewableItemsChanged and renderItem props.

import { observe } from 'react-native-observable-list';
import Carousel, {
  type TCarouselProps,
} from 'react-native-reanimated-carousel';

function ViewableCarousel<T>(
  props: TCarouselProps<T> & {
    onViewableItemsChanged?: ({
      changed,
      viewableItems,
    }: {
      changed: ViewToken[];
      viewableItems: ViewToken[];
    }) => void;
  }
) {
  return (
    <Carousel
      {...props}
      onProgressChange={(offsetProgress: number, absoluteProgress: number) => {
        const index = Math.floor(absoluteProgress);
        const item = props.data[index];
        props.onViewableItemsChanged?.({
          changed: [],
          viewableItems: [{ item, index, isViewable: true, key: undefined }],
        });
        props.onProgressChange?.(offsetProgress, absoluteProgress);
      }}
    />
  );
}

const ObservableCarousel = observe(ViewableCarousel);

Example3 (FlashList)

It can also be used with FlashList, which offers a similar interface to FlatList.

In this case, you have to use a key in the internal list to prevent recycling.

import { observe } from 'react-native-observable-list';
import { FlashList } from '@shopify/flash-list';

const ObservableFlatList = observe(FlashList);

const Example3 = () => {
  return (
    <ObservableFlatList
      // ...
      renderItem={({ index: outerIndex }) => {
        return (
          <ObservableFlatList
            key={`inner-list-${outerIndex}`} // do not recycle !!
            // ...
          />
        );
      }}
    />
  );
};

Example4 (Detect First Item)

The callback function registered with the useIsFirst hook is triggered when the first item changes.

import { useState } from 'react';
import { FlatList, View } from 'react-native';
import { observe, useIsFirst } from 'react-native-observable-list';

const ObservableFlatList = observe(FlatList);

const Item = ({
  color,
  onFirst,
  height = 500,
}: {
  color: string;
  onFirst: () => void;
  height?: number;
}) => {
  useIsFirst(() => {
    onFirst();
  }, [color]);

  return <View style={{ backgroundColor: color, height }} />;
};

const data = ['blue', 'yellow', 'green', 'orange'];

const Example4 = () => {
  const [first, setFirst] = useState<string | undefined>(undefined);
  return (
    <ObservableFlatList
      data={data}
      renderItem={({ item: color }) => {
        return (
          <Item
            color={color}
            onFirst={() => {
              setFirst(color);
            }}
          />
        );
      }}
    />
  );
};

key

If a keyExtractor is provided, its return value is used as the key to store visibility status. It is called with (item, index), the same contract as FlatList.

Without a keyExtractor, keys are extracted with the same rules as FlatList's default key extractor: item.key if present, then item.id, then String(index).

Known Issue

Visibility Judgment

FlatList considers an item component as non-existent if it doesn't occupy space (e.g., <View />), even if the component is within the viewport.

On the other hand, FlashList considers an item as existent as long as it is within the viewport, even if it doesn't occupy space.

const Item = ({ id }: any) => {
  useInViewPort(() => {
    console.log(`${id} mount !!`);
    return () => {
      console.log(`${id} unmount !!`); // executed on FlatList
    };
  });

  const [hide, setHide] = useState(false);

  useEffect(() => {
    setTimeout(() => {
      setHide(true);
    }, 1000);
  }, []);

  if (hide) return <View />;
  return <View style={{ width: 10, height: 10 }} />;
};

Therefore, the cleanup function of useInViewPort was not designed to run when unmounted, but depending on the situation, it may still run during unmounting.

Type Cast

I was unable to cast the input type directly to add the observeEnabled property.

Despite trying various casting methods, both FlatList and FlashList could not infer the item type.

As a result, I had to override the property that uses the item type in props.

Therefore, while there should be no issue when wrapping FlatList, if you wrap a custom component (e.g., Example2), newly added properties like getItemLayout, CellRendererComponent, and getItemType might be recognized by the newly created component.

You may need to check if the original component has the same properties and override them as needed, like in Example2.

Observable Lists Must Connect to the Root

I will define the component wrapped with observe as Observable List.

All lists up to the root of the item where the useInViewPort hook is used must be Observable Lists.

If the nested list is not an Observable List, its items will not be properly tracked.

In the example below, when the inner FlatList becomes visible on the screen, it is considered that all of its items are being in the viewport.

const Example1 = () => {
  return (
    <ObservableFlatList
      style={styles.container}
      data={outerData}
      ListHeaderComponent={<View style={styles.header} />}
      renderItem={({ index: outerIndex }) => {
        if (outerIndex % 10) return <TrackableItem label={`${outerIndex}`} />;
        return (
          <FlatList // not allowed
            data={innerData}
            horizontal
            renderItem={({ index: innerIndex }) => {
              return (
                <TrackableItem
                  width={100}
                  label={`${outerIndex}-${innerIndex}`}
                />
              );
            }}
          />
        );
      }}
    />
  );
};

Sticky

FlatList excludes items that start being managed as Sticky Headers from viewableItems.

Because of this, even if an item with the sticky property is visible on the screen, it may be perceived as gone.

FlashList duplicates sticky items and overlaps them.

Because of this, items may appear twice and be perceived as disappearing.

useIsFirst in FlashList

In FlashList, sticky items are duplicated and overlaid with an absolute position.

This may cause state changes to not function properly. As a result, Example 4 will not work when using FlashList.

Additionally, since sticky headers in FlashList are treated as items outside of the list, any item being tracked as the first header will be considered "first" once it scrolls up by the height of the sticky header.

License

MIT


Made with create-react-native-library