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

@apolloscooters/react-native-wheel-picker-native

v0.1.1

Published

The real iOS and Android wheel picker for React Native, built so it never reloads under a touch

Readme

@apolloscooters/react-native-wheel-picker-native

The real iOS and Android wheel picker for React Native, built so it never reloads under a touch.

  • Native: UIPickerView on iOS and NumberPicker on Android, so it looks, scrolls and works with VoiceOver and TalkBack exactly like the platform's own wheel.
  • Stable: the wheel on screen is never rebuilt while someone might be touching it. That is what made other wheel pickers crash on iOS (see Why this exists).
  • Simple: one component, three props to get started, typed values.
  • New Architecture: a Fabric component. No Expo modules or extra setup needed.

| iOS | Android | | --- | --- | | | |

Installation

yarn add @apolloscooters/react-native-wheel-picker-native

or

npm install @apolloscooters/react-native-wheel-picker-native

Then install the iOS pod:

cd ios && pod install

Android needs nothing else: autolinking picks the package up on the next build.

Expo

Works in development builds (npx expo prebuild or EAS Build). It does not work in Expo Go, which only ships the native code Expo bundles.

npx expo install @apolloscooters/react-native-wheel-picker-native
npx expo prebuild

Requirements

| | Minimum | | --- | --- | | React Native | 0.80, with the New Architecture (the default since 0.76, the only option since 0.82) | | iOS | The minimum your React Native version supports | | Android | API 24. Text colour and font size need API 29 (Android 10); older versions use the system style |

Usage

import { useState } from 'react';
import { WheelPicker } from '@apolloscooters/react-native-wheel-picker-native';

const speeds = [10, 15, 20, 25, 30].map((speed) => ({
  label: `${speed} km/h`,
  value: speed,
}));

export function SpeedLimit() {
  const [speed, setSpeed] = useState(20);

  return (
    <WheelPicker
      items={speeds}
      selectedValue={speed}
      onValueChange={setSpeed}
      accessibilityLabel="Speed limit"
    />
  );
}

That is the whole API for most screens: give it rows, tell it which one is selected, and update your state when the user picks another.

Strings work too

const fruits = [
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana' },
  { label: 'Cherry', value: 'cherry' },
];

<WheelPicker items={fruits} selectedValue={fruit} onValueChange={setFruit} />;

The value type is inferred from items, so onValueChange receives a string here and a number above.

Choosing a value only when the user confirms

A common pattern is a dialog with Cancel and Save. Keep a draft value for the wheel and copy it when Save is pressed:

const [saved, setSaved] = useState(20);
const [draft, setDraft] = useState(saved);

<WheelPicker items={speeds} selectedValue={draft} onValueChange={setDraft} />
<Button title="Save" onPress={() => setSaved(draft)} />

Styling

<WheelPicker
  items={fruits}
  selectedValue={fruit}
  onValueChange={setFruit}
  textColor="#E4572E"
  fontSize={26}
  style={{ height: 180, width: 240 }}
/>

The wheel is 216 points tall on iOS and 180 on Android unless you set a height. It fills the width it is given.

For dark mode, pass the colour your theme uses for text, for example textColor={isDark ? '#FFFFFF' : '#000000'}. When no colour is set, iOS uses the system label colour, which already follows dark mode.

Props

| Prop | Type | Default | Description | | --- | --- | --- | --- | | items | { label: string; value: string \| number }[] | required | The rows, top to bottom. Values should be unique. | | selectedValue | string \| number | first row | The value of the row to show. If no item has this value, the first row is shown. | | onValueChange | (value, index) => void | | Called when the wheel comes to rest on a row the user scrolled or tapped to. | | textColor | ColorValue | platform label colour | Colour of the row text. | | fontSize | number | platform size | Font size of the row text, in points. On iOS the default follows the user's text size setting. | | accessibilityLabel | string | | The name VoiceOver and TalkBack read for the wheel, for example "Speed limit". | | style | StyleProp<ViewStyle> | { height: 216 } on iOS, { height: 180 } on Android | Size and layout. | | testID | string | | For end to end tests. |

The package also exports the WheelPickerItem, WheelPickerProps and WheelPickerValue types.

How it behaves

  • onValueChange fires when the wheel stops, not for every row it passes while spinning. It does not fire when you change selectedValue yourself.
  • Changing selectedValue moves the wheel without an animation. If the user is touching or spinning the wheel at that moment, the change waits until the next update after the wheel has stopped, so it never fights the user's finger.
  • Changing items, textColor or fontSize replaces the wheel with a freshly built one showing selectedValue. You can change them at any time, for example to switch units from km/h to mph, but avoid changing them on every render: create items with useMemo or outside the component.
  • Empty items shows an empty wheel and never calls onValueChange.

Testing with Jest

The package ships modern JavaScript modules. The React Native Jest preset only transforms packages whose names start with react-native, so add the @apolloscooters scope to transformIgnorePatterns:

// jest.config.js
module.exports = {
  preset: '@react-native/jest-preset', // 'react-native' on older templates
  transformIgnorePatterns: [
    'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@apolloscooters)/)',
  ],
};

If you already have a transformIgnorePatterns list, add @apolloscooters to it. No mock is needed: the React Native preset renders native components as plain host elements, so render(<WheelPicker … />) works in tests and you can find it by testID.

Accessibility

The wheel is the platform control, so screen readers handle it the way users expect:

  • VoiceOver (iOS) announces the accessibilityLabel, the current row and that it is adjustable. Swiping up or down moves one row.
  • TalkBack (Android) reads the current row and lets the user move through the rows.

Always pass an accessibilityLabel that says what the wheel chooses. Without it, a screen reader user only hears a value.

Why this exists

The iOS wheel from @react-native-picker/picker can crash under Fabric with EXC_BAD_ACCESS in -[UIPickerView hitTest:withEvent:], on table cells that were already released (react-native-picker/picker#627). It happens when a touch arrives while the wheel's rows are being rebuilt, for example when the user taps a wheel that is still spinning.

We hit it in production in the Apollo Scooters app and found several ways the wheel could be rebuilt under a finger:

  1. Every prop update, including the one sent for each row the wheel passes, reloaded the wheel.
  2. React Native recycled the native view between screens, because the library's recycling opt out was declared in a way React Native never reads.
  3. The selection was set from code with an animation while the user could still be touching it.

This package is built around one rule: a wheel that is on screen is never rebuilt. In practice:

  • each wheel loads its rows once,
  • a change of rows, colour or font swaps in a new wheel instead of reloading the old one,
  • the selection only moves from code when nobody is touching the wheel,
  • the native view is never recycled.

The same design ships in the Apollo Scooters app, where an automated stress test ran it hundreds of times in a row on a real iPhone and on the simulator without a crash: open the wheel in a dialog, flick it twice, tap it while it is still coasting, close with Save or Cancel, repeat. This package runs the same test against its example app.

Migrating from @react-native-picker/picker

// Before
<Picker selectedValue={speed} onValueChange={setSpeed}>
  {speeds.map((s) => (
    <Picker.Item key={s.value} label={s.label} value={s.value} />
  ))}
</Picker>

// After
<WheelPicker items={speeds} selectedValue={speed} onValueChange={setSpeed} />

Differences to know about:

  • Rows are an items array rather than child elements.
  • On Android this package always shows a wheel. @react-native-picker/picker shows a dropdown or dialog there.
  • itemStyle becomes textColor and fontSize.
  • onValueChange fires once the wheel stops, not while it scrolls.

Troubleshooting

"Unimplemented component" or nothing renders. The New Architecture is off, which is only possible on React Native 0.80 and 0.81. Turn it back on (newArchEnabled=true in android/gradle.properties, and RCT_NEW_ARCH_ENABLED=1 pod install for iOS), then rebuild the app.

"WheelPickerNativeView" was not found in the UIManager. The native code is not in your build. Run pod install for iOS, then rebuild the app. Reloading JavaScript is not enough after adding a native package.

It does not work in Expo Go. That is expected, see Expo.

Android ignores textColor or fontSize. The device runs Android 9 or older, where NumberPicker does not offer these settings. The wheel uses the system style there.

Example app

The example folder has a small app with a basic wheel, programmatic changes, a unit switch, styling and a wheel inside a modal.

yarn
yarn example ios       # or: yarn example android

Contributing

Bug reports and pull requests are welcome. See CONTRIBUTING.md for how to set up the project and run the checks.

License

MIT, see LICENSE.

Made by Apollo Scooters.