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

@splithub/react-native

v0.5.1

Published

A/B testing and analytics for React Native and Expo apps on iOS and Android: show a variant, track screens and events, works offline.

Readme

@splithub/react-native

Experiments and analytics for React Native and Expo apps. Variants are decided on the device before the first render (a local hash, no network call), screens and events are counted in the same reports as your website, and events wait on the device while it is offline.

Install

Expo:

npx expo install @splithub/react-native @react-native-async-storage/async-storage expo-application expo-device

Bare React Native:

npm i @splithub/react-native @react-native-async-storage/async-storage
cd ios && pod install

React Native 0.72 or newer. Storage keeps the visitor (and their variant) between launches, plus events not sent yet. The package uses react-native-mmkv when it is installed, else AsyncStorage. expo-application and expo-device report the app version and the device model; without Expo, pass them yourself (see below).

Quick start

import { SplitHubProvider, Experiment, Variant, useTrack } from '@splithub/react-native';

export default function App() {
  return (
    <SplitHubProvider publicKey="pk_..." property="sh-x1h78w">
      <Home />
    </SplitHubProvider>
  );
}

function Home() {
  const track = useTrack();
  return (
    <>
      <Experiment id="welcome">
        <Variant name="control"><Text>Welcome to the shop.</Text></Variant>
        <Variant name="short"><Text>Shop in two taps.</Text></Variant>
      </Experiment>
      <Button title="Get started" onPress={() => track('Get Started')} />
    </>
  );
}

publicKey is your project's public key (pk_...) from the dashboard, property the id of your app's analytics property. Create the welcome experiment in the dashboard: until it runs, everyone sees the control.

The hooks are the same as in @splithub/react: useVariant, useValue, useTrack, useIdentify, useScreen, useAssignment, useSplitHub.

With a config file

If you keep experiments in splithub.config.ts (and run the splithub CLI), pass the config instead:

import config from './splithub.config';

<SplitHubProvider config={config} property="sh-x1h78w">

App info without Expo

<SplitHubProvider publicKey="pk_..." app={{ appId: 'com.acme.shop', version: '2.4.1', build: '412' }}>

app also takes device (mobile | tablet | desktop), storage (an MMKV instance, AsyncStorage or any synchronous getItem / setItem / removeItem store) and extra targeting context.

Launch

AsyncStorage is read once into memory on launch, so variants stay synchronous. The provider renders fallback (default nothing) during that read. To start it earlier, call prepare() before the splash screen hides:

import { prepare } from '@splithub/react-native';
import * as SplashScreen from 'expo-splash-screen';

SplashScreen.preventAutoHideAsync();
prepare().finally(() => SplashScreen.hideAsync());

With MMKV there is nothing to wait for.

The very first launch also waits up to 400 ms for the dashboard's experiments (control meanwhile), then decides once. Later launches use the copy saved on the device and refresh it in the background.

Screens

A screen is a page in the analytics reports: the url is the route, the title the screen name.

Expo Router, once in app/_layout.tsx:

function Screens() {
  useExpoRouterTracking(); // `/`, `/product/42`, ...
  return <Stack />;
}

export default function RootLayout() {
  return (
    <SplitHubProvider publicKey="pk_...">
      <Screens />
    </SplitHubProvider>
  );
}

React Navigation:

const ref = useNavigationContainerRef();
const tracking = useNavigationTracking(ref); // `/Home`, `/Settings`, ...

<NavigationContainer ref={ref} {...tracking}>

useNavigationTracking must run inside <SplitHubProvider>. Anything else: useScreen('Checkout').

The same as one line each, which is what npx splithub init writes:

import { SplitHubProvider, ExpoRouterScreens, NavigationScreens } from '@splithub/react-native';

<SplitHubProvider config={config}>
  <ExpoRouterScreens />        {/* app/_layout.tsx */}
  <Stack />
</SplitHubProvider>

<SplitHubProvider config={config}>
  <NavigationContainer>
    <NavigationScreens />      {/* finds the container; or navigationRef={ref} */}
    <Tabs />
  </NavigationContainer>
</SplitHubProvider>

Targeting by app version

Every event carries the app's platform and version, so an experiment can target them. In the dashboard's targeting editor add an app version or platform condition; in a config file:

flow: {
  start: 'new-app',
  nodes: {
    'new-app': { type: 'condition', conditions: [{ field: 'app_version', op: 'gte', value: '2.4' }], then: 'split', else: 'out' },
    split: { type: 'split', outputs: [{ weight: 50, to: 'a' }, { weight: 50, to: 'b' }] },
    a: { type: 'variant', variant: 'control' },
    b: { type: 'variant', variant: 'new-checkout' },
    out: { type: 'skip' },
  },
},

Versions compare part by part (2.10.0 is after 2.9). Only builds that ship the new variant's code should enter: older versions get the control and count as nothing.

Why app experiments live in the cloud

A website deploys in minutes; an app waits for store review and for users to update. So the variants ship inside the build, and everything else lives in the dashboard: start, pause, weights, values, targeting and new value experiments reach installed apps on the next launch. Nothing is fetched while rendering; the app uses the copy it saved on the last launch.

Debug builds send nothing

Like vite or next dev on the web, a development build (NODE_ENV=development, every Metro debug bundle) sends nothing to SplitHub Cloud, so your own taps never count as visitors. Variants still render, and assignment works the same. To see events arrive, run a release build:

npx expo start --no-dev --minify              # Expo Go / dev client, production JS
npx expo run:ios --configuration Release
npx expo run:android --variant release

Background and offline

Events are sent in small batches, and right away when the app goes to the background. Without a connection they stay on the device (up to 1000) and go out on a later launch with their original time. Coming back after 30 minutes in the background starts a new session.

Example

examples/expo-app in the SDK repository: Expo Router, an experiment, screens and an event.