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

@epifanovmd/anchor-list

v3.1.0

Published

Виртуализированный список для React Native: удержание видимой позиции, прилипающие якоря на обеих кромках, состояние на UI-потоке.

Readme

AnchorList

npm CI license platforms

Виртуализированный список для React Native, который держит на месте не экран, а строку, на которую смотрит пользователь.

Что это и для чего

AnchorList — замена FlatList для экранов, где контент меняется не только снизу: переписки, ленты с подгрузкой истории, списки с растущими ячейками. У обычного списка при любом изменении выше вьюпорта всё, что на экране, уезжает вниз: подгрузилась страница истории — прыжок на её высоту; докачалась картинка в сообщении — ещё один; открылась клавиатура — контент под ней или дёрнулся с опозданием на кадр.

Здесь позиция привязана к строке, а не к смещению. Перед любым изменением список запоминает строку у кромки, после изменения находит её по ключу и возвращает на то же место экрана. Сдвиг делает нативный ScrollView в той же транзакции, что и раскладка, поэтому жест и инерция не прерываются, а рывка между кадрами нет. Прилипание, клавиатура, подгрузка и состояние на UI-потоке построены вокруг этой же привязки и не мешают друг другу.

Список подходит, когда нужна одна колонка и точная позиция. Если нужны колонки, masonry или web, он не подойдёт — см. Чего нет.

Что умеет

Держит видимую строку на месте. Вставка и удаление строк выше вьюпорта, рост уже отрисованных ячеек — экран не двигается. Две причины включаются раздельно: maintainVisibleContentPosition={{ data, size }}.

Прилипание на обеих кромках сразу. Заголовки дат у верхней кромки, аватар группы у нижней. Прилипать может вся строка или один объект внутри неё. Отступ кромки — shared value, поэтому якорь едет вместе с клавиатурой. Проп sticky.

Клавиатура одним числом. Передайте insetEnd shared value с высотой панели и клавиатуры — распорка в конце контента, подъём смещения, отступ индикатора скролла и отступ прилипшего якоря считаются от него сами, на UI-потоке.

Экран переписки без inverted. Открыться внизу, прижать короткую переписку к низу, приезжать к новому сообщению, если пользователь стоял у конца, и грузить историю вверх — всё это пропы, а не перевёрнутый список. Переход к цитате подсвечивает её: scrollToKey({ key, highlight: true }) — подсветка загорается, когда строка доехала до кадра, а как она выглядит, решает строка через useAnchorListItemHighlight().

Подгрузка, которая не стреляет дважды. Пороги у обеих кромок в долях экрана, одно срабатывание на жест, общий замок на две кромки, повторное срабатывание, когда страница пришла, а до кромки по-прежнему близко.

Состояние без рендеров. Смещение, скорость, расстояния до кромок, видимый диапазон — всего больше двух десятков величин — отдаются в shared values. Тень под навбаром, кнопка «вниз» и индикатор прокрутки не стоят ни одного рендера. Те же значения доступны и в React с адресной подпиской. Строке о себе отвечает useAnchorListItemVisibility() — доля строки во вьюпорте каждым кадром, с учётом клавиатуры и прилипших якорей: параллакс и затухание у кромок без JS.

Виртуализация с переработкой. Расчёт диапазона и позиций живёт вне React; скролл, сдвинувший одну строку, перерисовывает один контейнер. Контейнеры переиспользуются по типам строк, объявленные размеры не измеряются вовсе, буфер растёт со скоростью броска. Горизонтальная ось — тем же пропом horizontal.

Открытие там, где закрыли, без лишнего кадра. Снимок getScrollAnchor() — ключ строки у кромки и её смещение — возвращается в initialScroll по ключу и переживает подгрузку истории. sizeCache держит измеренные размеры между монтированиями: при возврате на экран видимые строки известны заранее, и список показывается сразу, а не после доводки.

Диагностика на каждую механику. Восемь каналов логов и встроенный замер кадров, чтобы разбирать «дёрнулось» по числам, а не на глаз. Подробности — Диагностика и Производительность.

Пример

Экран переписки целиком. Обязательных пропов четыре: data, renderItem, keyExtractor, estimatedItemSize. Остальные включают механики по одной.

import { AnchorList } from "@epifanovmd/anchor-list";

<AnchorList
  data={messages}
  renderItem={renderItem}
  keyExtractor={message => message.id}
  estimatedItemSize={92}
  // Открыться внизу и прижать короткую переписку к низу.
  initialScroll={{ type: "end" }}
  alignItemsAtEnd
  // Новое сообщение приезжает само, если пользователь стоял у конца.
  maintainScrollAtEnd={{ onlyWhenAtEnd: true, animated: true }}
  // История сверху и выросшие ячейки не двигают то, что на экране.
  maintainVisibleContentPosition={{ data: true, size: true }}
  onStartReached={loadOlder}
  // Заголовки дат прилипают к верхней кромке.
  sticky={[{ edge: "start", indices: dayIndices }]}
  // Панель ввода и клавиатура: контент едет вместе с ними.
  insetEnd={keyboardInset}
  recycleItems
  style={{ flex: 1 }}
/>;

Объекты-настройки и renderItem в реальном коде нужно мемоизировать; полная версия этого экрана с хуками клавиатуры и кнопкой «вниз» — в Рецептах.

Состояние в анимацию, без рендеров:

const scrollOffset = useSharedValue(0);
const sharedValues = useMemo(() => ({ scrollOffset }), [scrollOffset]);

const shadow = useAnimatedStyle(() => ({
  opacity: Math.min(1, scrollOffset.value / 24),
}));

<AnchorList sharedValues={sharedValues} ... />;

Чем отличается от других списков

| | FlatList | FlashList | AnchorList | | --- | --- | --- | --- | | Удержание позиции при вставке сверху | Только minIndexForVisible | Есть | Якорь по ключу, снимается до изменения; свой выбор якоря shouldRestorePosition | | Удержание при росте уже отрисованных ячеек | Нет | — | Отдельный флаг size; в замере видно, сколько сдвига доехало | | Прилипание | Только верх | Только верх | Обе кромки, вся строка или объект внутри, отступ shared value | | Клавиатура и панель ввода | Своими руками | Своими руками | insetEnd одним shared value | | Экран переписки | inverted | Есть | Прямой список: initialScroll, alignItemsAtEnd, maintainScrollAtEnd | | Смещение скролла наружу | onScroll в JS | onScroll в JS | Shared value на UI-потоке, плюс два десятка других величин | | Видимость строки | Пороги в JS | Пороги в JS | Пороги в JS и доля строки во вьюпорте как shared value, с учётом клавиатуры | | Восстановление позиции | По индексу | По индексу | Снимок по ключу getScrollAnchor, кэш измерений между монтированиями | | Подсветка цели перехода | Нет | Нет | scrollToKey({ highlight }) — загорается, когда строка доехала; стиль решает строка | | Переработка ячеек | Нет | Да | Да, по типам | | Колонки, masonry | Да | Да | Нет | | Pull-to-refresh | Да | Да | Нет | | Web | Да | Да | Нет |

Установка

yarn add @epifanovmd/anchor-list

Нужны React Native 0.78+, React 19+, react-native-reanimated 4+ и react-native-worklets; работает только новая архитектура. Нативного кода в пакете нет: линковка не нужна, Expo Go работает. Плагин worklets должен стоять последним в babel — см. Установку.

Чего нет

  • Колонок и masonry: раскладка одномерная.
  • inverted: та же задача решается прямым списком, см. Миграцию.
  • Pull-to-refresh и анимаций вставки строк.
  • Web: удержание позиции опирается на нативный maintainVisibleContentPosition, которого нет в react-native-web.
  • Горизонтального списка под RTL: нативное удержание позиции не учитывает RTL.

Полный список с причинами — Ограничения.

Документация

| Раздел | О чём | | --- | --- | | Быстрый старт | Первый список, обязательные пропы, частые ошибки | | Справочник пропов | Все пропы, типы, значения по умолчанию | | Рецепты | Экран переписки, лента, восстановление позиции, кнопка «вниз» — целиком | | Удержание позиции | Компенсация по данным и по размерам | | Прилипание | Два режима, обе кромки, отступы | | Отступы и клавиатура | insetEnd и всё, что от него считается | | Подгрузка | Пороги, спиннеры, подгрузка вверх | | Скролл и позиционирование | Стартовая позиция, программный скролл, прилипание к концу | | Состояние списка | sharedValues, state, все сигналы | | Миграция | С FlatList, FlashList и LegendList | | Симптомы | Симптом → причина → что делать | | Механика | Как всё устроено, без кода |

Все разделы — в docs/. Стенды на устройстве — по одному на механику — в example/: yarn example ios или android.

Участие

Правка поведения начинается с падающего теста, а заканчивается зелёным yarn check. Как устроен цикл правки, коммиты и выпуск — в Ведении библиотеки и CONTRIBUTING.md.

Лицензия

MIT