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-turbo-preferences

v2.0.0

Published

Share preferences between your React Native app and iOS widgets, watch apps & app extensions. Modern TurboModule for NSUserDefaults (App Groups) and Android SharedPreferences.

Downloads

699

Readme

React Native Turbo Preferences

npm version License: MIT Build Status React Native Platform

⚡ Share preferences between your React Native app and iOS widgets, watch apps, and app extensions — App Groups (NSUserDefaults) on iOS, SharedPreferences on Android. Built as a TurboModule for React Native's New Architecture.

Your React Native JS code can't be seen by a WidgetKit widget, a watchOS app, or a share extension — the only bridge between them is a shared native store. This library gives you direct, typed access to that store: UserDefaults(suiteName:) App Group containers on iOS and named SharedPreferences files on Android.

It's also a great fit whenever native code (SDKs, Settings.bundle, Android home-screen widgets) needs to read values your JS writes — or plain fast key-value storage for app preferences.

import { createStore, reloadWidgets } from 'react-native-turbo-preferences';

const appGroup = createStore('group.com.yourcompany.yourapp');

await appGroup.setInt('streak', 42); // a real integer — no string parsing
await appGroup.setBoolean('goalReached', true);
await reloadWidgets(); // your widget refreshes now, not "eventually"

🌟 Features

  • 📲 App Group Sharing — Write from JS, read from your iOS widget, watch app, or extension
  • 🔌 Expo Config Plugin — Auto-configures the App Group entitlement on prebuild, no Xcode needed
  • 🗂 Multiple Stores — Handles to the default store, named files, and App Groups, all usable side by side
  • 🔢 Typed Values — Booleans, ints, and doubles stored as real native types
  • 📡 Change Listeners — React to writes from native code; hooks stay in sync across components
  • 🔄 Widget RefreshreloadWidgets() triggers WidgetKit / AppWidget updates from JS
  • 🪝 React Hooks — Reactive usePreference* hooks with automatic cross-component sync
  • 🚀 New Architecture Native — A true TurboModule, not an old bridge module running through interop
  • 📦 Lightweight — Wraps NSUserDefaults / SharedPreferences directly, no custom storage format
  • 🔒 Type Safe — Written in TypeScript with full type definitions

🤔 Why this library?

The two libraries most apps use for App Group / native preference sharing haven't shipped a release in years:

| | react-native-turbo-preferences | react-native-shared-group-preferences | react-native-default-preference | | ------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Actively maintained | ✅ | ❌ Last release Sept 2023 | ❌ Last release June 2022 | | New Architecture | ✅ Native TurboModule | ⚠️ Old bridge (via interop layer) | ⚠️ Old bridge (via interop layer) | | iOS App Groups | ✅ UserDefaults(suiteName:) | ✅ | ✅ | | Android backend | ✅ SharedPreferences (app-sandboxed) | ⚠️ Public external-storage JSON file, needs storage permissions, readable by other apps | ✅ SharedPreferences | | Expo | ✅ Config plugin (guide below) | ❌ "Doesn't work for Expo" (their README) | ⚠️ Undocumented | | Multiple stores at once | ✅ Handle-based (createStore) | ❌ | ❌ Global setName switch | | Typed values | ✅ bool / int / double stored natively | ⚠️ JSON blob | ❌ Strings only | | Change listeners | ✅ Incl. native writes | ❌ | ❌ | | React hooks | ✅ Auto-syncing | ❌ | ❌ | | Widget refresh from JS | ✅ reloadWidgets() | ❌ | ❌ | | TypeScript | ✅ Written in TS | ❌ | ⚠️ Type definitions only |

📦 Installation

npm install react-native-turbo-preferences
# or
yarn add react-native-turbo-preferences

Bare React Native:

npx pod-install

Expo: works with development builds / EAS. To share data with widgets or extensions, add the config plugin to app.json:

{
  "expo": {
    "plugins": [
      [
        "react-native-turbo-preferences",
        { "appGroup": "group.com.yourcompany.yourapp" }
      ]
    ]
  }
}

Requires the New Architecture (default since React Native 0.76). Old-architecture apps can't use this library.

🚀 Quick Start

Stores

import Prefs, { createStore } from 'react-native-turbo-preferences';

// Top-level functions use the default store
await Prefs.set('username', 'Hamza');
const username = await Prefs.get('username'); // "Hamza"

// Named stores are handles — use as many as you need, at the same time
const settings = createStore('settings');
const appGroup = createStore('group.com.yourcompany.yourapp');

await settings.set('theme', 'dark');
await appGroup.setInt('streak', 42); // visible to your widget

Hooks

import { usePreferenceString, createStore } from 'react-native-turbo-preferences';

const appGroup = createStore('group.com.yourcompany.yourapp');

function Profile() {
  const [username, setUsername, hasUsername, clearUsername] =
    usePreferenceString('username');
  // Scope a hook to a store by passing the handle:
  const [workout] = usePreferenceString('lastWorkout', appGroup);

  return (
    <View>
      <Text>{username ?? 'Not set'}</Text>
      <Button title="Set" onPress={() => setUsername('Hamza')} />
    </View>
  );
}

Hooks subscribe to change events: two components using the same key stay in sync, and the UI updates when native code writes the key.

📲 Sharing data with an iOS Widget (App Groups)

This is the flagship use case: your React Native app writes a value, and your WidgetKit widget (or watch app / share extension / App Clip) reads it natively. Both sides just need to point at the same App Group.

1. Enable the App Group

Bare React Native: in Xcode, select your app target → Signing & Capabilities+ CapabilityApp Groups → add a group like group.com.yourcompany.yourapp. Repeat for your widget/extension target with the same group id.

Expo: use the built-in config plugin (see Installation) — it adds the entitlement during expo prebuild / EAS Build, merging with any existing App Groups. To create the widget extension target itself in an Expo project, use a target plugin such as @bacons/apple-targets, and give the widget target the same App Group entitlement.

2. Write from React Native

import { createStore } from 'react-native-turbo-preferences';

const appGroup = createStore('group.com.yourcompany.yourapp');

await appGroup.setInt('streak', 42); // stored as a real integer
await appGroup.setBoolean('goalReached', true); // stored as a real boolean
await appGroup.set('lastWorkout', 'Push day'); // strings via set()

The handle only touches the App Group container — the rest of your app keeps using the default store (or other handles) at the same time.

3. Read from your widget (Swift)

Because values are stored as real native types, your widget reads them with the normal typed UserDefaults accessors — no string parsing:

struct Provider: TimelineProvider {
  func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
    let defaults = UserDefaults(suiteName: "group.com.yourcompany.yourapp")
    let streak = defaults?.integer(forKey: "streak") ?? 0
    let goalReached = defaults?.bool(forKey: "goalReached") ?? false
    let workout = defaults?.string(forKey: "lastWorkout") ?? "—"
    // build your timeline entry from these values …
  }
}

4. Refresh the widget

iOS decides when widget timelines refresh on their own. To make the widget pick up your new values immediately, call reloadWidgets() after writing:

import { reloadWidgets } from 'react-native-turbo-preferences';

await appGroup.setInt('streak', 43);
await reloadWidgets(); // WidgetCenter.shared.reloadAllTimelines()
await reloadWidgets('StreakWidget'); // …or only one kind: reloadTimelines(ofKind:)

On Android, reloadWidgets() broadcasts ACTION_APPWIDGET_UPDATE to all of your app's widget providers (the kind argument is iOS-only and ignored).

Android: sharing with native code

On Android, createStore('my_file') maps to getSharedPreferences("my_file", MODE_PRIVATE) — the same file any native code in your own app (a Glance/home-screen widget, a headless service, an SDK) can read:

val prefs = context.getSharedPreferences("my_file", Context.MODE_PRIVATE)
val streak = prefs.getInt("streak", 0)
val goalReached = prefs.getBoolean("goalReached", false)
val workout = prefs.getString("lastWorkout", null)

Data stays inside your app's sandbox. (Unlike libraries that write a world-readable JSON file to external storage, other apps can't read, edit, or delete it — and no storage permissions are required.)

📖 API Reference

createStore(name?: string): PreferenceStore

Creates a handle to a preference store. Handles are cheap JS objects — create as many as you need and use several stores at the same time, with no global state.

  • name — iOS: UserDefaults suite (e.g. an App Group). Android: SharedPreferences file name. Omit for the default store.

A PreferenceStore exposes the full API scoped to that store:

| Method | Description | Returns | | ------------------------ | -------------------------- | ----------------------------------------- | | get(key) | Retrieve string | Promise<string \| null> | | set(key, value) | Store string | Promise<void> | | clear(key) | Delete key | Promise<void> | | contains(key) | Check existence | Promise<boolean> | | setBoolean(key, value) | Store native boolean | Promise<void> | | getBoolean(key) | Retrieve boolean | Promise<boolean \| null> | | setInt(key, value) | Store native int32 | Promise<void> | | getInt(key) | Retrieve integer | Promise<number \| null> | | setDouble(key, value) | Store native double/float | Promise<void> | | getDouble(key) | Retrieve double | Promise<number \| null> | | setMultiple(values) | Store multiple strings | Promise<void> | | getMultiple(keys) | Retrieve multiple | Promise<Record<string, string \| null>> | | clearMultiple(keys) | Delete multiple | Promise<void> | | getAll() | All keys/values (as strings) | Promise<Record<string, string>> | | clearAll() | Wipe the store | Promise<void> | | addListener(fn) | Changes in this store only | EventSubscription | | name | The store's name (null = default) | string \| null |

The same functions are exported at the top level (import { get, set, … }) operating on the default store.

Typed values

Booleans and numbers are stored as real native types (not strings), so native readers use their normal typed accessors: integer(forKey:) / bool(forKey:) on iOS, getInt / getBoolean on Android.

await store.setBoolean('darkMode', true);
await store.setInt('streak', 42); // must be a 32-bit integer, rejects otherwise
await store.setDouble('progress', 0.75);

const streak = await store.getInt('streak'); // 42, or null if missing
  • iOS: setBool / setInteger / setDouble — Android: putBoolean / putInt / putFloat
  • Android has no putDouble, so doubles round-trip with Float precision on Android
  • Typed getters resolve null when the key is missing or holds an incompatible type

Change listeners

import { addPreferenceChangeListener } from 'react-native-turbo-preferences';

// All stores: event.store says which one changed (null = default)
const sub = addPreferenceChangeListener((event) => {
  console.log(`"${event.key}" changed in ${event.store ?? 'default'}`);
});

// One store:
const storeSub = appGroup.addListener((event) => {
  console.log('widget data changed:', event.key);
});

sub.remove();

Fires for writes made through this module and by native code (widgets, watch apps, SDKs). event.key is null when the whole store changed at once (e.g. clearAll).

Platform behavior:

  • iOS: NSUserDefaultsDidChangeNotification, diffed per key against per-store snapshots. Fires for changes made within your app's process.
  • Android: SharedPreferences.OnSharedPreferenceChangeListener per store.

Widgets

reloadWidgets(kind?: string): Promise<void>

Asks the OS to refresh your home-screen widgets so they pick up newly written values.

  • iOS: WidgetCenter.shared.reloadAllTimelines(), or reloadTimelines(ofKind:) when kind is passed
  • Android: broadcasts ACTION_APPWIDGET_UPDATE to all of the app's widget providers (kind ignored)

Deprecated: setName(name)

Switches the store that the top-level functions point at. Prefer createStore()setName is a global switch shared by every call in your app, which invites subtle bugs when two features use different namespaces. Existing code keeps working.

Migration note (v2): the selection now lives in JS and resets on app restart — this was always Android's behavior; iOS used to persist it across launches.

🪝 React Hooks

All value hooks return [value, setValue, contains, clear], load automatically on mount, and stay in sync: every hook instance watching a key updates when that key changes — from another component, the imperative API, or native code.

| Hook | Value type | Notes | | --------------------------------- | ---------------- | ---------------------------------------- | | usePreferenceString(key, store?) | string \| null | | | usePreferenceNumber(key, store?) | number \| null | stored as string, parsed on read | | usePreferenceBoolean(key, store?) | boolean \| null | accepts "true"/"false"/"1"/"0" | | usePreferenceObject<T>(key, store?) | T \| null | JSON (de)serialization built in | | usePreferenceNamespace() ⚠️ deprecated | — | drives the global setName store |

store is an optional PreferenceStore handle from createStore(); omit it to use the default store (or the one selected with setName).

import {
  usePreferenceBoolean,
  usePreferenceObject,
} from 'react-native-turbo-preferences';

interface UserSettings {
  fontSize: number;
  language: string;
}

function SettingsScreen() {
  const [notifications, setNotifications] = usePreferenceBoolean('notifications');
  const [settings, setSettings] = usePreferenceObject<UserSettings>('userSettings');

  return (
    <View>
      <Switch value={notifications ?? false} onValueChange={setNotifications} />
      <Button
        title="Bigger text"
        onPress={() =>
          setSettings({ language: 'en', ...settings, fontSize: 18 })
        }
      />
    </View>
  );
}

🔀 Migrating

From v1 of this library

  • setName() and all top-level functions keep working. New code should use createStore().
  • The default export is now a wrapper object with the same methods (previously the raw native module). If you called methods on the default export, nothing changes; if you accessed the native module directly, switch to named imports.
  • On iOS, the setName selection no longer persists across app restarts (it never did on Android).

From react-native-default-preference

// before                                   // after
DefaultPreference.setName('group.myapp');   const store = createStore('group.myapp');
DefaultPreference.get('key');               store.get('key');
DefaultPreference.set('key', 'v');          store.set('key', 'v');
DefaultPreference.clearAll();               store.clearAll();

getMultiple returns an object here (not an array), and setMultiple takes [{ key, value }] pairs.

From react-native-shared-group-preferences

// before
SharedGroupPreferences.setItem('key', value, 'group.myapp');
SharedGroupPreferences.getItem('key', 'group.myapp');

// after
const store = createStore('group.myapp');
store.set('key', JSON.stringify(value)); // it JSON-serialized for you
JSON.parse((await store.get('key')) ?? 'null');

On Android you also gain sandboxed storage — the old library wrote a world-readable JSON file to external storage.

🔒 Security

This library stores values in NSUserDefaults (iOS) and SharedPreferences (Android), which are not encrypted.

Do NOT store: passwords, API tokens, payment data, or personal identification data.

For secure storage use: Keychain on iOS (react-native-keychain, expo-secure-store) and EncryptedSharedPreferences on Android (react-native-encrypted-storage).

🌍 Platform Support

| Platform | Support | Notes | | ------------ | ------- | ---------------------------------------- | | iOS | ✅ | NSUserDefaults, App Groups, WidgetKit | | Android | ✅ | SharedPreferences, AppWidget broadcast | | React Native | ✅ | New Architecture only (default since 0.76) | | Expo | ✅ | Development builds & EAS, config plugin |

🎮 Example App

The example/ folder is an interactive tour of the whole API — stores, typed values, live change-event log, and hook synchronization:

yarn install
cd example
yarn ios      # or: yarn android

📊 Performance

Operations go straight to NSUserDefaults / SharedPreferences over JSI (no bridge serialization), and both platforms batch writes natively (apply() on Android, background persistence on iOS). For high-frequency writes of large blobs consider MMKV instead — this library's niche is preferences that native code needs to read.

🗺 Roadmap

  • [x] ✅ Batch operations, namespaces, TypeScript, hooks
  • [x] ✅ Expo config plugin — App Group entitlement from app.json
  • [x] ✅ reloadWidgets() — WidgetKit / AppWidget refresh from JS
  • [x] ✅ Typed values (bool/int/double) stored as real native types
  • [x] ✅ Change listeners + auto-syncing hooks
  • [x] ✅ Handle-based stores (createStore) — no more global setName
  • [ ] 🔜 Typed hooks — back usePreferenceNumber/usePreferenceBoolean with native typed storage
  • [ ] 🔜 Cross-process change detection on iOS (KVO) — see changes a widget writes while the app runs
  • [ ] 🔜 Swift Package Manager support (tracking the RN SPM RFC)
  • [ ] 🔜 Store deletion (deleteSharedPreferences / remove suite)

❓ FAQ

Yes. Hooks subscribe to store change events, so every hook instance watching a key updates when that key changes — whether the write came from another component, the imperative API, or native code. For cross-cutting logic outside components, use addPreferenceChangeListener.

Yes — that's what createStore() is for. Each handle is independent; there is no global switching:

const defaults = createStore();
const appGroup = createStore('group.com.your.app');
await defaults.set('lastScreen', 'home');
await appGroup.setInt('streak', 42);

Use usePreferenceObject<T> (JSON built in) or store JSON strings manually:

await store.set('user', JSON.stringify({ name: 'John', age: 30 }));
const user = JSON.parse((await store.get('user')) ?? 'null');

iOS controls widget timeline refreshes. Call reloadWidgets() after writing to force an immediate refresh. Also confirm both targets use the exact same App Group id.

No — NSUserDefaults and SharedPreferences store data in plain text inside your app's sandbox. Never store secrets; see Security.

🤝 Contributing

  1. Fork and branch: git checkout -b feature/amazing-feature
  2. Make your changes and run yarn test && yarn typecheck && yarn lint
  3. Commit using conventional commits and open a PR

📄 License

MIT — see LICENSE.

📞 Support


Made with ❤️ by hamzamekk