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

@shaquillehinds/react-native-essentials

v1.15.1

Published

Essential constants, styles and hooks for react native.

Readme

@shaquillehinds/react-native-essentials

A batteries‑included toolkit for React Native and Expo apps: screen‑relative layouts, a typography scale, buttons, press wrappers with spring feedback, providers for localization / portals / long‑running event tracking, gestures, animation helpers, MMKV storage accessors, and a bag of hooks and pure utilities.

The design goal is that you almost never write View, Text, TouchableOpacity, or pixel values by hand. Layouts are described with percentage‑based props that scale with the device, typography is a fixed scale of named sizes, and everything composes.


Table of contents


AI agent rules

The package ships a rules file written for AI coding agents (Claude Code, Cursor, Codex, Copilot, etc.) at rules/AGENT_RULES.md. It tells an agent to use this package for layouts, text, buttons, spacing, and sizing instead of raw React Native primitives, and lists every export so it cannot invent APIs. Point your agent at it with any of the following.

Copy it into your project (recommended)

npx rne-rules            # writes ./AGENTS.md
npx rne-rules cursor     # writes ./.cursor/rules/react-native-essentials.mdc (alwaysApply)
npx rne-rules claude     # writes ./.claude/rules/react-native-essentials.md
npx rne-rules docs/ai/rn-essentials.md   # custom path

Add --force to overwrite an existing file. Re-run after upgrading the package to pick up rule changes.

Reference it without copying (Claude Code)

CLAUDE.md supports @path imports, so a single line keeps the rules in sync with the installed version:

# CLAUDE.md

@node_modules/@shaquillehinds/react-native-essentials/rules/AGENT_RULES.md

Reference it from a generic AGENTS.md

Before writing any React Native UI, read and follow
node_modules/@shaquillehinds/react-native-essentials/rules/AGENT_RULES.md.

Installation

npm install @shaquillehinds/react-native-essentials
# or
yarn add @shaquillehinds/react-native-essentials

Peer dependencies (checked at runtime by checkRequiredDependencies()):

npx expo install react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-mmkv react-native-svg

The package also imports eventemitter3 and buffer.

You can fail fast during development:

import { checkRequiredDependencies } from '@shaquillehinds/react-native-essentials';
if (__DEV__) checkRequiredDependencies();

checkRequiredDependencies({ dependencies }) accepts a custom list of { name, packageName, required } if you want to extend the check. checkDependency(packageName) returns a boolean, and createDependencyError(featureName, packageName) builds a consistent error for optional features.


App setup

Fonts

Text components set fontFamily to the literal fontStyle value. The FontStyle union is:

'Thin' | 'Extra Light' | 'Light' | 'Regular' | 'Medium' | 'SemiBold' | 'Bold' | 'ExtraBold' | 'Black'

Register your fonts under those exact names (only the ones you use):

import { useFonts } from 'expo-font';

const [fontsLoaded] = useFonts({
  Regular: require('./assets/fonts/Sen-Regular.ttf'),
  Medium: require('./assets/fonts/Sen-Medium.ttf'),
  SemiBold: require('./assets/fonts/Sen-SemiBold.ttf'),
  Bold: require('./assets/fonts/Sen-Bold.ttf'),
  ExtraBold: require('./assets/fonts/Sen-ExtraBold.ttf'),
});

Because fontStyle is passed straight through, you can also register additional custom names (e.g. a handwriting face) and pass them as fontStyle, casting past the union if you use TypeScript.

Globals

Importing the package executes essentials/utils/global, which installs:

| Global | Behaviour | | ------------------------------- | --------------------------------------------------------------------------------------------------- | | is(value) | Returns value if truthy, otherwise undefined. Handy for conditional JSX: {is(cond) && <X />}. | | isDef(value) | value !== undefined | | errMsg(error) | Error → message, string → itself, anything else → JSON.stringify | | Array.prototype.filterMap(fn) | Map that skips falsy source items and drops null/undefined results |

Add a declaration file so TypeScript knows about them:

// @types/globals.d.ts
type NonFalsy<T> = Exclude<T, 0 | '' | false | null>;
declare module globalThis {
  var is: <T>(value: T) => NonFalsy<T> | undefined;
  var isDef: (value: unknown) => boolean;
  var errMsg: (error: unknown) => string;
}

Providers at the root

import { GestureHandlerRootView } from 'react-native-gesture-handler';
import {
  LocalizationProvider,
  PortalProvider,
} from '@shaquillehinds/react-native-essentials';

export function AppProviders({ children }) {
  return (
    <GestureHandlerRootView>
      <LocalizationProvider
        sourceLanguage="en"
        targetLanguage="en"
        translation={async ({ sourceLanguage, targetLanguage, text }) => {
          // call your translation backend; must resolve to a string
          return text;
        }}
      >
        <PortalProvider>{children}</PortalProvider>
      </LocalizationProvider>
    </GestureHandlerRootView>
  );
}

Only add the providers you use. LocalizationProvider is required for translate / useTranslation; PortalProvider for usePortal / usePortalComponent; EventTrackerProvider for event tracking.


Sizing model

All sizing is derived from Dimensions.get('screen') so that the same numbers produce proportionally identical layouts on every device.

Relative functions

Static versions (computed once at import):

| Function | Returns | | -------------------------------------- | -------------------------------------------------------------------------------------- | | relativeX(n) | n% of screen width | | relativeY(n) | n% of screen height (adjusted by half the screen/window height difference) | | relativeShort(n) | n% of MIN_DIMENSION | | relativeLong(n) | n% of MAX_DIMENSION | | normalize(n) | relativeLong(n * scale) rounded to the nearest pixel (scale = 0.11519078473722104) | | normalizeShort(n) | relativeShort(n * scale) rounded to the nearest pixel | | relativeXWorklet, relativeYWorklet | Worklet‑marked equivalents for Reanimated |

Reactive versions come from useDeviceOrientation() (see Hooks) and update when the device rotates. Layout and friends use the reactive versions internally.

normalize is what makes the font scale and radius tokens feel like "design points": on a device whose longer dimension is ~868pt, normalize(16) ≈ 16.

Spacing tuples

padding and margin accept a Spaces tuple — [number, number?, number?, number?] — following CSS shorthand order, where every value is a percentage:

| Tuple | top | right | bottom | left | | -------------- | --- | ----- | ------ | ---- | | [a] | a | a | a | a | | [v, h] | v | h | v | h | | [t, h, b] | t | h | b | h | | [t, r, b, l] | t | r | b | l |

Top/bottom values are resolved with relativeY (percent of screen height); left/right values with relativeX (percent of screen width). transformSpacing({ margin, padding, orientation? }) and spacerStyles(type, { orientation? })(...values) are exported if you need the same resolution in your own code.

Tokens

type BorderSize = 'razor' | 'thin' | 'medium' | 'large';
// borderSizes: razor 0.1%, thin 0.25%, medium 0.5%, large 0.75%  (of MIN_DIMENSION)

type RadiusSize =
  | 'edgy'
  | 'sharp'
  | 'medium'
  | 'soft'
  | 'curvy'
  | 'round'
  | 'full';
// radiusSizes: normalize(5 | 10 | 15 | 20 | 25 | 30), full = relativeLong(100)

type FontSize =
  | 'headingL'
  | 'headingM'
  | 'headingS'
  | 'titleL'
  | 'titleM'
  | 'titleS'
  | 'bodyL'
  | 'bodyM'
  | 'bodyS';
// fontSizes: normalize(26 | 24 | 22 | 20 | 18 | 16 | 14 | 12 | 10)

type ButtonSize = 'small' | 'medium' | 'large' | 'wide' | 'auto';

borderSizes, radiusSizes, fontSizes, and buttonSizes are exported as plain objects for use inside StyleSheet.create.

buttonSizes (padding via normalizeShort):

| size | paddingHorizontal | paddingVertical | fontSize | borderRadius | width | | ------ | ----------------- | --------------- | -------- | ------------ | ------------------- | | small | 30 | 15 | bodyS | edgy | — | | medium | 60 | 20 | bodyL | sharp | — | | large | 120 | 25 | titleS | sharp | — | | wide | 120 | 25 | titleM | sharp | relativeShort(88) | | auto | 60 | 25 | titleM | sharp | '100%' |

Device constants

SCREEN_WIDTH, SCREEN_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT, MAX_DIMENSION, MIN_DIMENSION, aspectRatio, initialOrientation ('portrait' | 'landscape'), isSmallDevice (width < 375 or height < 750), isLargeDevice (width > 1100), isTablet (aspect ratio ≤ 1.6), isIpad, isIOS, isAndroid, isWeb.

Note: isIOS, isAndroid, isWeb are true | undefined (not false), so they can be spread into conditional style arrays.


Layouts

Layout

The foundation. Renders a View, or a ScrollView when scrollable, or an animated equivalent when animated.

import { Layout } from '@shaquillehinds/react-native-essentials';

<Layout
  width={90} // number → % of screen width; string → passed through ('100%')
  height={20} // number → % of screen height; string → passed through
  square={8} // sets width AND height to n% of the longer screen dimension
  flex={[1, 0, 'auto']} // [flex] | [flex, flexShrink] | [flex, flexShrink, flexBasis]
  center // alignItems: 'center'
  centerX // justifyContent: 'center'
  spaceBetween // justifyContent: 'space-between'
  padding={[1, 3]}
  margin={[2, 0, 0, 0]}
  backgroundColor="#111"
  borderColor="#333"
  borderWidth="thin"
  borderRadius="soft"
  absolute
  top={0}
  left={0}
>
  ...
</Layout>;

Props (LayoutProps<Scrollable>)

| Prop | Type | Notes | | ----------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | width, height | DimensionValue | Numbers are percentages of the screen (relativeX / relativeY); strings and other values pass through. | | square | number | Percentage of the longer screen dimension applied to both width and height (used when width/height are not numbers). | | flex | [number] \| [number, number] \| [number, number, DimensionValue] | flex, flexShrink, flexBasis. | | center | boolean | alignItems: 'center' — the cross axis. | | centerX, spaceCenter | boolean | justifyContent: 'center' — the main axis. | | spaceBetween, spaceEven, spaceStart, spaceEnd | boolean | justifyContent variants. Precedence: spaceEven > spaceBetween > centerX/spaceCenter > spaceStart > spaceEnd. | | wrap | boolean | flexWrap: 'wrap' (default 'nowrap'). | | flexDirection | FlexStyle['flexDirection'] | Prefer RowLayout. | | alignSelf | FlexAlignType | | | absolute | boolean | position: 'absolute' | | top, bottom, left, right | DimensionValue | Passed through as‑is. | | padding, margin | Spaces | Percentage tuples (see above). | | backgroundColor | string | Applied to both the container and the content style. | | borderColor | string | | | borderWidth | BorderSize | Token, not a number. | | borderRadius | RadiusSize | Token, not a number. | | loading | boolean \| LoadingIndicatorProps | Replaces the whole layout with a LoadingIndicator. Only backgroundColor and the object's props are used. | | skeleton | boolean \| { colors?: [string, string] } | Wraps children in a SkeletonViewIndicator with the layout's computed styles. Children are rendered invisibly to size the shimmer, so pass mock data while loading (see Skeleton loading). | | scrollable | boolean | Renders a ScrollView. When true, the remaining props are typed as ScrollViewProps; otherwise ViewProps. | | animated | boolean | Use an animated wrapper. | | animatedType | 'reanimated' \| 'react-native' | Default 'reanimated'. | | animatedStyle | StyleProp<AnimatedStyle<ViewStyle>> | Used instead of style when animated && animatedType === 'reanimated'. | | style | StyleProp<ViewStyle> | Applied last. Ignored when using Reanimated animation (use animatedStyle). |

Everything else is forwarded to the underlying View / ScrollView (onLayout, onTouchStart, pointerEvents, collapsable, contentContainerStyle, ...).

Skeleton loading

A skeleton must have the exact dimensions and shape of the data it stands in for. Layout does not try to guess that shape: when skeleton is on it renders the layout's normal styles and its children at opacity: 0, then paints the shimmer over the area those children occupy. The skeleton therefore only ever knows about layout, never about data.

The pattern is:

  1. While loading, render the same component tree you would render with real data, but feed it mock data with realistic lengths (a plausible name, a two‑line description, a real‑looking price).
  2. Pass skeleton={isLoading} to the Layout / RowLayout / TouchableLayout that wraps that tree.
  3. When the data arrives, swap the mock data for the real data and set skeleton to false. Nothing else changes, so there is no layout shift.
const MOCK_USER = { name: 'Firstname Lastname', bio: 'A short two‑line bio that is about this long so the card keeps its height.' };

function UserCard({ user, loading }: { user?: User; loading: boolean }) {
  const data = loading ? MOCK_USER : user!;
  return (
    <RowLayout skeleton={loading} padding={[2, 4]} borderRadius="soft" backgroundColor="#fff">
      <Avatar uri={data.avatarUrl} />
      <Layout flex={[1]}>
        <Heading>{data.name}</Heading>
        <Body>{data.bio}</Body>
      </Layout>
    </RowLayout>
  );
}

Rules of thumb:

  • Never render an empty <Layout skeleton />. With no children (and no explicit width/height) it has nothing to measure and collapses.
  • Mock data should fill the component the way real data would, including text length and list item count. Too little mock data gives a skeleton that is smaller than the loaded content.
  • Put skeleton on the smallest layout that wraps one unit of loading content (a card, a row, a list item) rather than on the whole screen, so static chrome such as headers stays visible.
  • For a single loading text node inside loaded chrome, use the text component's own skeleton prop instead (see Typography).
  • children must not do anything with side effects (fetching, analytics) when rendered with mock data; they are still mounted, only hidden.
  • Use loading instead only when a spinner is genuinely wanted and the placeholder size does not matter.

The centring rule

center is alignItems, centerX is justifyContent. That means:

  • In a column Layout, center centres horizontally and centerX centres vertically.
  • In a RowLayout, center centres vertically and centerX centres horizontally.
  • center centerX centres on both axes in either case.

Style composition

  • Plain view: style={[contentStyle, viewStyle, style]}
  • Scroll view: style={[viewStyle, style]}, contentContainerStyle={[contentStyle, yourContentContainerStyle]} and overflow: 'visible' is forced.

contentStyle holds padding, alignItems, flexDirection, flexWrap, justifyContent, and backgroundColor. viewStyle holds margin, width/height, alignSelf, position, flex, offsets, border, and backgroundColor.

RowLayout

Layout with flexDirection: 'row' appended after your style, so it always renders as a row. Same generic Scrollable parameter and props.

<RowLayout center spaceBetween margin={[2, 0, 0, 0]}>
  <Title>Journals</Title>
  <Press onPress={onImport}>
    <ImportIcon />
  </Press>
</RowLayout>

ScreenLayout

Layout with { display: 'flex', flex: 1 } prepended, plus:

| Prop | Notes | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | safe | Wraps the layout in SafeAreaProvider → SafeAreaView with edges={['top', 'bottom']}. The safe view gets flex: 1, overflow: 'visible', and a backgroundColor copied from the backgroundColor prop or from style.backgroundColor. | | scrollable | As in Layout. |

<ScreenLayout safe backgroundColor={theme.background} padding={[0, 3]}>
  ...
</ScreenLayout>

// centred splash / empty state
<ScreenLayout center centerX backgroundColor={theme.background}>
  <Logo />
  <Heading margin={[5, 0]}>Welcome</Heading>
</ScreenLayout>

AnimatedLayout

const rStyle = useAnimatedStyle(() => ({ opacity: progress.value }));
<AnimatedLayout animatedStyle={rStyle} center padding={[2]}>
  ...
</AnimatedLayout>;

Layout with animated fixed to true (Reanimated). Props: Omit<LayoutProps, 'animated'> & { animatedStyle? }. Remember that style is not applied in this mode.

TouchableLayout

A TouchableOpacity that accepts the same sizing/alignment/spacing/border/loading/skeleton props as Layout (no scrollable, no animated). All TouchableOpacityProps are forwarded.

SeparatorLayout

<SeparatorLayout lineColor="#444" margin={[3, 0]}>
  <Body customColor="#888">OR</Body>
</SeparatorLayout>

| Prop | Default | | ------------------- | ------------------------------------- | | lineColor | '#222222' | | lineWidth | 0.5 | | lineOpacity | 0.8 | | + all LayoutProps | defaults: center, margin={[2, 0]} |

Renders RowLayout → line, children, line.


Typography

| Component | Default fontSize | Default fontStyle | fontSize accepted | | ---------- | ------------------ | ------------------- | -------------------------------------- | | BaseText | bodyM | Regular | any FontSize | | Body | bodyM | Regular | bodyS | bodyM | bodyL | | Title | titleM | Medium | titleS | titleM | titleL | | Heading | headingS | SemiBold | headingS | headingM | headingL |

BaseTextProps (extends React Native TextProps and Spacing):

| Prop | Type | Notes | | ------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------- | | fontSize | FontSize | Resolved via useFontSizes(). | | fontStyle | FontStyle | Written to fontFamily. | | customColor | string | color. | | center | boolean | textAlign: 'center' (otherwise 'left'). | | lineHeight | 'short' \| 'tall' | 1.05× / 1.35× the font size. | | letterSpacing | 'wide' \| 'extraWide' | 0.7 / 1.2. | | numberOfLines | number | | | onPress | (e) => void | | | animate | boolean | Renders Reanimated Animated.Text. | | animatedStyle | StyleProp<AnimatedStyle<TextStyle>> | Applied between the computed style and style. | | translate | boolean | Wraps string children in TranslateText (see LocalizationProvider). | | padding, margin | Spaces | Percent tuples (static relativeX/relativeY). | | skeleton | SkeletonLoadingIndicatorProps | Object, not boolean. Wraps the text in a SkeletonViewIndicator sized by the (hidden) text, so pass mock text of realistic length. margin moves to the wrapper; colors, disableAnimation, style inside the object go to the wrapper. | | style | StyleProp<TextStyle> | Applied last. |

<Heading fontSize="headingL" customColor="white" margin={[5, 0]}>Welcome</Heading>
<Title fontSize="titleS">Entries</Title>
<Body numberOfLines={1} customColor={theme.typeface.secondary} margin={[0, 1]}>
  {entry.journalName}
</Body>
<Body translate center fontSize="bodyL">{description}</Body>

Text skeletons follow the same rule as Layout skeletons: the text is rendered at opacity: 0 with its real font size, line height and padding, and the shimmer covers exactly that box. Enable it with an object (an empty one is fine) and render mock text of the length you expect:

<Heading skeleton={loading ? {} : undefined}>{loading ? 'Placeholder headline' : post.title}</Heading>
<Body numberOfLines={2} skeleton={loading ? { colors: ['#222', '#333'] } : undefined}>
  {loading ? 'Two lines of placeholder body copy that is roughly as long as a real excerpt would be.' : post.excerpt}
</Body>

Use a text skeleton for a single text node inside otherwise‑loaded chrome (a price, a username). When a whole card is loading, put skeleton on the wrapping Layout instead so all children shimmer as one block. Because ButtonProps extends BaseTextProps, skeleton on a BaseButton is forwarded to its label only; the button frame stays visible.

Recommended pattern: wrap BaseText once per role in your project so colours come from your theme, then use those wrappers everywhere.

export function Title(props: PropsWithChildren<TitleTextProps>) {
  return (
    <BaseText
      {...props}
      customColor={theme.typeface.primary}
      fontSize={props.fontSize ?? 'titleM'}
      fontStyle={props.fontStyle ?? 'SemiBold'}
    />
  );
}

TranslateText, LocalizationComponent, and TranslationComponent are also exported; they are the internals behind translate and can be used directly.


Buttons

BaseButton

<BaseButton
  buttonSize="wide"
  backgroundColor={['#4A87F2', '#7B4AF2']} // string or gradient array
  customFontColor="#fff"
  borderRadius="round" // RadiusSize or number
  leftComponent={<Icon />}
  leftComponentGap={8}
  loading={saving}
  onPress={save}
>
  Save
</BaseButton>

ButtonProps extends Omit<BaseTextProps, 'style'> (so translate, fontSize, fontStyle, numberOfLines, margin, padding all apply to the label) and adds:

| Prop | Type | Default / notes | | --------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------- | | buttonSize | ButtonSize | 'medium'. Drives padding, font size, radius, width (see Tokens). | | backgroundColor | string \| string[] | An array renders an AbsoluteLinearGradient behind the label. | | gradientStart, gradientEnd, gradientOpacities | | Forwarded to the gradient. | | customFontColor | string | Label and spinner colour. | | fontStyle | FontStyle | 'Medium' | | fontSize | FontSize | From buttonSize. Line height is 1.3×. | | textStyle | StyleProp<AnimatedStyle<TextStyle>> | Merged into the label's animatedStyle. | | borderColor | string | 'transparent' | | borderWidth | BorderSize | 'thin' | | borderRadius | RadiusSize \| number | From buttonSize. Inner view uses radius - 1 with overflow: 'hidden'. | | alignSelf | FlexAlignType | 'center' | | shadow | ShadowStylesProps | Applied via shadowStyles. | | leftComponent, rightComponent | JSX.Element | Rendered inside the row around the label. | | leftComponentGap, rightComponentGap | number | Label marginLeft / marginRight. | | loading | boolean \| LoadingIndicatorProps | Hides the label (opacity 0), shows a small ActivityIndicator, disables press. | | disabled | boolean | Sets activeOpacity 0.5 and disables. | | activeOpacity | number | 0.8 | | enableRapidPress | boolean | Forwarded to Press (plain TouchableOpacity, no double‑tap protection). | | animate | boolean | Inner view becomes Reanimated Animated.View. | | style | StyleProp<AnimatedStyle<ViewStyle>> | Applied to the inner view. | | onPress | | |

Children default to the string 'Submit'.

Structure: Press (margin, shadow, border, width, alignSelf) → inner view (radius, background/gradient) → row (padding, leftComponent, BaseText, rightComponent) + optional absolute spinner.

Project wrapper pattern:

export function PrimaryButton({
  customFontColor,
  backgroundColor,
  ...props
}: PropsWithChildren<ButtonProps>) {
  return (
    <BaseButton
      {...props}
      customFontColor={customFontColor ?? theme.typeface.primary}
      backgroundColor={backgroundColor ?? theme.lightBackground}
    />
  );
}

Press wrappers

Four components share the same press model. They listen to onTouchStart / onTouchMove / onTouchEnd on an animated view rather than using Pressable.

| Component | Renders | Animation | | ------------------- | ----------------------------------------------------------------- | ---------------------------------------------- | | Press | Reanimated Animated.View | spring scale → 0.95, opacity → activeOpacity | | RNPress | RN Animated.View | 200ms timing, native driver | | PressableLayout | AnimatedLayout (all LayoutProps) | Reanimated | | RNPressableLayout | Layout animated animatedType="react-native" (all LayoutProps) | RN Animated |

Shared props:

| Prop | Default | Notes | | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | onPress(e: GestureResponderEvent) | | Fires on touch end after activationDelay. | | onLongPress(snapshot) | | Receives a GestureResponderNativeEventSnapshot (pageX, pageY, locationX, locationY, timestamp, identifier, target, force), not the synthetic event. | | longPressDuration | 800 ms | | | activationDelay | 50 ms | Wait before firing onPress so a move‑cancel can win. | | activeOpacity | 0.9 | | | disabled | | Renders at opacity 0.5 and ignores touches. | | disableAnimation | | Skip the scale/opacity feedback. | | disableDoubleTapProtection | | By default a second activation within minDoubleTapProtectionDuration is ignored. | | minDoubleTapProtectionDuration | 750 ms | | | stopPropagation, preventDefault, persist | | Applied to the touch events. | | style | | For Press/RNPress this is the animated view's style. For PressableLayout it is combined with the press animation and passed as animatedStyle‑equivalent internally. |

Press only: enableRapidPress renders a plain TouchableOpacity with onPress/onLongPress/activeOpacity and no protection.

A touch that moves more than 10px from its start point cancels the press.

<Press onPress={onImport}><ImportIcon size={22} /></Press>

<PressableLayout
  center spaceBetween padding={[1, 3]} borderRadius="soft" backgroundColor="#1a1a1a"
  onPress={() => open(item)}
  onLongPress={() => showOptions(item)}
>
  ...
</PressableLayout>

Inputs

BaseInput

A RowLayout wrapper around a TextInput with focus/error border colours and slots on either side.

<BaseInput
  backgroundColor={theme.lightBackground}
  textInputProps={{
    placeholder: 'Enter password',
    autoCapitalize: 'none',
    onChangeText,
  }}
  focusedBorderColor="#4A87F2"
  blurredBorderColor="transparent"
  erroredBorderColor="#E55774"
  hasError={!!error}
  LeftComponent={<LockIcon />}
  refTextInput={inputRef}
  margin={[2, 0]}
/>

BaseInputProps = the following + LayoutProps:

| Prop | Notes | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | | backgroundColor | required | | textInputProps | required TextInputProps; placeholder defaults to 'Type here...'. | | hasError | Switches to erroredBorderColor. | | focusedBorderColor / blurredBorderColor / erroredBorderColor | Defaults #4A87F2 / transparent / #E55774. Border width is 1. | | LeftComponent, RightComponent | Each reduces the input's width by 10% (100% → 90% → 80%). | | TextInputComponent | Custom input renderer receiving TextInputProps & { ref }. | | refTextInput | MutableRefObject<TextInput \| null> — receives the inner ref. | | refStateInput | Ref<StateInputRef> — when provided (and no TextInputComponent), renders StateTextInput. | | refStateInputValidator | (text) => boolean — gate for StateTextInput updates. |

Default padding is [1.5, 4] on iOS and [0.4, 4] on Android; tapping the row focuses the input.

StateTextInput

A TextInput that owns its value in state and exposes it through refStateInput:

const stateRef = useRef<StateInputRef>(null);
<StateTextInput
  refStateInput={stateRef}
  refStateInputValidator={(t) => t.length <= 10}
  placeholder="Name"
/>;
// later
stateRef.current?.value;
stateRef.current?.setValue('');

StateInputRef = { value: string; setValue: Dispatch<SetStateAction<string>> }. If the validator returns false, the value is not updated and onChangeText is not called.


Indicators, injectors, utilities, icons

LoadingIndicator

Full‑size (100% × 100%) centred ActivityIndicator size="large".

| Prop | Notes | | ---------------------------------------------- | --------------------------------- | | TopComponent, BottomComponent | Rendered above/below the spinner. | | backgroundColor, opacity, animationColor | | | absolute | position: 'absolute' |

Used by Layout loading. Example of a full‑screen overlay:

<Layout
  loading={{
    absolute: true,
    opacity: 0.9,
    backgroundColor: theme.background,
    animationColor: theme.accent,
    BottomComponent: (
      <Layout margin={[2, 0, 0, 0]}>
        <Title fontSize="titleL">{message}</Title>
      </Layout>
    ),
  }}
/>

SkeletonViewIndicator

Shimmering gradient overlay (Reanimated + react-native-svg). Props: colors?: [string, string] (default ['#ECECEC', '#FBFAFE']), disableAnimation?, children, plus ViewProps. Used by Layout / TouchableLayout skeleton.

It sizes itself from its children: they are rendered at opacity: 0 (still mounted, still measured) and an absolutely‑filled gradient is drawn on top with overflow: 'hidden'. It has no intrinsic size of its own, so always give it either children populated with mock data or an explicit width/height via style.

<SkeletonViewIndicator style={{ borderRadius: radiusSizes.soft }}>
  <Body>{loading ? 'Placeholder text of the same length' : text}</Body>
</SkeletonViewIndicator>

Prefer Layout skeleton over using this directly; it gives you the layout's spacing, radius and sizing for free.

ViewDimensionsInjector

Measures itself with onLayout and renders renderItem(layoutRectangle) once dimensions are known. Props: renderItem, absolute?, justifyContent? (default 'center'), aligntItems? (default 'center'; note the prop spelling).

ComponentMounter

Delayed mount/unmount of a component, driven either by props or by an imperative controller.

const mounter = useRef<ComponentMounterController>(null);
<ComponentMounter
  ref={mounter}
  component={<Toast />}
  unMountDelayInMilliSeconds={200}
/>;
mounter.current?.mountComponent({ onOpen });
mounter.current?.unMountComponent({ duration: 300, onClose });
mounter.current?.hardUnMountComponent();

Props: component (required), showComponent?, setShowComponent?, onComponentShow?, onComponentClose?, mountDelayInMilliSeconds?, unMountDelayInMilliSeconds?, mountDefault?, keepMountedOnReopen? (default false). When showComponent turns true while already mounted (for example a reopen inside the unmount delay), the default behaviour is to flip setShowComponent(false) and hard‑unmount on the next change; pass keepMountedOnReopen to cancel the pending unmount and stay mounted instead. Callbacks and delays are read from the latest props on every mount/unmount, so they do not need to be memoised.

RadioIcon

<RadioIcon
  isSelected={selected}
  selectedColor="#4A87F2"
  unSelectedColor="#D9D9D9"
  size={2}
  borderWidth="medium"
  alwaysShowCenter
/>

Built from two Layout square={...} borderRadius="full" views. size is a percentage of the longer screen dimension (default 2).


Modal building blocks

These are low‑level pieces for composing your own modals.

| Component | Purpose | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ModalWrapper | Absolute‑fill view with zIndex: maxZIndex. Props: enableBackgroundContentPress (pointerEvents="box-none"), useNativeModal (renders RN Modal visible transparent statusBarTranslucent wrapping a GestureHandlerRootView), disableAndroidBackButton, onRequestClose. | | ModalBackgroundAnimated | TouchableWithoutFeedback → Reanimated absolute‑fill view. Props: onPress, style, animatedStyle, avoidStatusBar (adds marginTop: StatusBar.currentHeight), children (default: rgba(0,0,0,.2) scrim). | | ModalForegroundWrapper | Plain view with zIndex: maxZIndex on iOS. Wrap your animated foreground in it so it sits above the background on iOS and so the keyboard doesn't shift it on Android. |

maxZIndex (999999999) is exported from styles.


TextStream

Types out a string character by character.

const ref = useRef<TextStreamRef>(null);
<TextStream
  ref={ref}
  autoStream
  streamCharacterDelay={15}
  CustomTextComponent={Body}
  onStreamFinish={done}
>
  {message}
</TextStream>;
ref.current?.stopStream();

Props (TextStreamProps<T extends TextProps>): autoStream, startStreamDelay, streamCharacterDelay (default 15ms), skipCharacterDelayInterval / skipCharacterDelayAmount (every N characters, skip the delay for M characters), CustomTextComponent, onStreamFinish, ref: { startStream, stopStream }, plus the wrapped text component's props. String children (or arrays of strings) are streamed; when the text grows the stream continues from the last index, when it shrinks it finishes immediately.


Providers

LocalizationProvider

<LocalizationProvider
  sourceLanguage="en"
  targetLanguage={userLanguage}
  translation={({ sourceLanguage, targetLanguage, text }) => api.translate(...)}
  initialLanguagesRecord={bundledTranslations}
  initialLanguagesRecordRetriever={({ sourceLanguage, targetLanguage }) => api.bulk(...)}
>

| Prop | Type | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | sourceLanguage, targetLanguage | LanguageCode (keys of LanguageCodesEnglishMappings, e.g. 'en', 'en-US', 'fr') | | translation | ({ sourceLanguage, targetLanguage, text }) => Promise<string> | | initialLanguagesRecord? | LanguagesRecord = Partial<Record<LanguageCode, Record<hash, string>>> | | initialLanguagesRecordRetriever? | ({ sourceLanguage, targetLanguage }) => Promise<{ original: string; translated: string }[]> — fetched once per target language when nothing is cached for it |

Behaviour:

  • If sourceLanguage and targetLanguage share the same first two letters, translate returns the input unchanged.
  • Cache key is sha256(text.trim(), 'base64'); the record is persisted in MMKV under essentials-localization-<sourceLanguage> via createStorageAccessors.
  • Concurrent requests for the same text share one in‑flight promise.
  • Non‑string results are treated as errors; on error the trimmed input is returned.
  • Context value: { translate(text): Promise<string>, clearLocalCache() }. useLocalization() returns it or null.

Consumers: any text component / BaseButton with translate, useTranslation({ text }), or TranslateText directly.

PortalProvider

<PortalProvider unMountBufferTimeMS={100} updateBufferTimeMS={0}>

Renders children, then an absolute‑fill pointerEvents="box-none" host containing every mounted portal item.

  • usePortal(CustomPortalContext?) → { mount(key, element, onMount?), update(key, element), unmount(key, onUnMount?) } or null.
  • usePortalComponent({ name, Component, disable?, CustomPortalContext? }) mounts Component on first render, updates it when it changes, and unmounts on cleanup; returns the portal context.
  • unMountBufferTimeMS (default 100) delays removal so a late update can cancel it; updateBufferTimeMS throttles updates per item.
  • Pass CustomPortalContext (a context created from PortalContext's type) to run several independent portal hosts.

EventTrackerProvider

Persists "events" (uploads, exports, background jobs) in MMKV and polls their status while in_progress.

<EventTrackerProvider
  statusCheckFnRegistry={{
    upload: async (event, trigger) => ({ ...event, status: await checkUpload(event.id) }),
  }}
>

| Prop | Notes | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | statusCheckFnRegistry | Record<string, StatusCheckFn>; StatusCheckFn = (event, triggerType?) => Promise<EventTracker> | | maxStoredEventTrackers?, defaultMaxInProgressTime?, defaultStatusCheckInterval? | Declared props. |

EventTracker fields: id, name, status: 'in_progress' | 'done' | 'failed' | 'cancelled', description, createdAt, updatedAt, statusCheckFnId, and optional extraData, image, extra, type, url, expires, statusCheckInterval (default 30000ms), maxTimeInProgress, triggerStatusCheckFnOn?: ('expired' | 'maxTimeInProgress')[].

Behaviour: on mount, expired events are deleted and in‑progress ones start a Scheduler.Schedule. Each tick calls the registered function; a status change is stored and, if no longer in progress, polling stops. Exceeding maxTimeInProgress marks the event failed (optionally calling the check function with 'maxTimeInProgress'); passing expires deletes it (optionally calling with 'expired'). Missing registry entries delete the event.

Hooks: useEventTracker() → { addEventTracker(event), removeEventTracker(id), deleteEvent(event), clearEvents(), markEventsAsSeen() }; useTrackerEvents() → { events, seen }. Storage accessors eventsStorage, seenEventsStorage, unSeenEventsStorage are exported.

Data collection

Typed, context‑based accumulation of partial data (multi‑step forms):

type Signup = { email: string; name: string };
const CollectedDataContext =
  createContext<CollectedDataContextValue<Partial<Signup>>>(undefined);
const DataCollectionContext =
  createContext<DataCollectionContextValue<Partial<Signup>>>(undefined);
export const signup = createDataCollector<Signup>({
  CollectedDataContext,
  DataCollectionContext,
});

// <signup.Provider> ... </signup.Provider>
const { collectData, collectedDataRef } = signup.useDataCollection()!;
collectData({ key: 'email', value }); // or collectData(prev => ({ ...prev, name }))
const { collected } = signup.useCollectedData()!;

DataCollectionProvider, useDataCollection(ctx), useCollectedData(ctx) are also exported individually.


Render isolation

Lets a subtree subscribe to a parent's fast‑changing values without re‑rendering the parent's other children.

type Obs = { scrollY: number; isDragging: boolean };

function Owner() {
  const isolateRef = useIsolateRef<Obs>();
  const { startObserving, stopObserving } = useIsolateObservables({
    ref: isolateRef,
    observables: { scrollY, isDragging },
    isObserving: true,
  });
  return (
    <IsolateRefDependant ref={isolateRef}>
      <Header isolateRef={isolateRef} />
    </IsolateRefDependant>
  );
}

function Header({ isolateRef }: { isolateRef: IsolateRef<Obs> }) {
  const { observables, stopObservation } = isolateRef.current!.useObservation({
    scrollY: (y) => y > 100, // predicate: re-render only when it returns true
    isDragging: true, // re-render on any change
  });
  return <Title>{observables.scrollY}</Title>;
}
  • useIsolateObservables({ ref, observables, isObserving? }) emits an update through an EventEmitter whenever observables changes while observing, tracking which keys changed. Returns { startObserving, stopObserving }.
  • The ref exposes useObservation(subscribeTo?) → { observables, stopObservation } and get(key) (read the latest snapshot without subscribing).
  • IsolateRefDependant renders its children only once ref.current is populated (retries up to 5 times with increasing delay).

Types: IsolateObservables, IsolateRef<T>, IsolateRefObject<T>, IsolateRefData<T>, IsolateObserve<T>, UpdatedIsolateObservables, ChangedIsolateObservables.


Gestures

All three wrap children in a GestureDetector from react-native-gesture-handler.

| Component | Props | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DragGesture | onDragStart(e), onDrag(e), onDragEnd?(e) (pan gesture, worklet callbacks), minDistance? (default 1), disable?, enableContentScroll? (runs simultaneously with a Gesture.Native() so scroll views inside still scroll) | | SwipeGesture | direction: 'UP' \| 'DOWN' \| 'LEFT' \| 'RIGHT', onActivation(e) (fling, runOnJS) | | TwoFingerLongPressGesture | onActivation(e) (two pointers, 1000ms) |

<SwipeGesture direction="UP" onActivation={onSwipeUp}>
  <SwipeGesture direction="DOWN" onActivation={onSwipeDown}>
    <Layout width={90} height={10} />
  </SwipeGesture>
</SwipeGesture>

Animations

useDragAnimation

const { onDragStart, onDrag, dragAnimatedStyle, translationX, translationY } =
  useDragAnimation();
<DragGesture
  onDragStart={onDragStart}
  onDrag={(e) => onDrag({ posX: e.translationX, posY: e.translationY })}
>
  <AnimatedLayout animatedStyle={dragAnimatedStyle} square={10} />
</DragGesture>;

onDrag({ posX, posY, minPosX?, maxPosX?, minPosY?, maxPosY? }) clamps to ± half the screen by default. Also returns prevTranslationX/Y.

ArcSpinnerAnimation

<ArcSpinnerAnimation size={24} color="#999" /> — a rotating arc (1s linear loop).

AnimateComponent (React Native Animated)

Declarative wrapper that creates an Animated.Value, Animated.ValueXY, or a string‑interpolated value depending on initialPosition, builds the composition, and renders Animated.View with the style you derive.

const ref = useRef<AnimateComponentRef<number>>(null);
<AnimateComponent
  ref={ref}
  initialPosition={0}
  toPosition={[{ type: 'timing', toValue: 1, duration: 300, useNativeDriver: true }]}
  autoStart
  returnToStart
  loop={3}
  style={(value, { inputRange }) => ({ opacity: value })}
  onAnimationEnd={...}
>
  {children}
</AnimateComponent>
ref.current?.reverse();
  • initialPosition: number → AnimateValueComponent; string → AnimateStringValueComponent (uses useAnimatedStringValue, type limited to timing | spring); { x, y } → AnimateXYValueComponent.
  • toPosition: one config or an array (sequence). Config = RN TimingAnimationConfig | SpringAnimationConfig | DecayAnimationConfig with a type discriminator.
  • autoStart, returnToStart, loop (iterations), onAnimationEnd.
  • Ref: { start, stop, reset, reverse, setValue, value } (setValue is undefined for the string variant).

SVG path animations

AnimateSVGPathValueComponent (mode: 'InterpolatePathProps') animates a single 0→1 value and lets you interpolate any PathProps:

<AnimateSVGPathValueComponent
  mode="InterpolatePathProps"
  animationConfig={{ type: 'timing', duration: 800, useNativeDriver: true }}
  autoStart
  pathProps={(value, { inputRange }) => ({
    d: PATH,
    strokeDashoffset: value.interpolate({ inputRange, outputRange: [100, 0] }),
  })}
/>

AnimateSVGPathValuesComponent (mode: 'AnimatedPathProps') animates several path props from from to a list of to values, in parallel or sequence:

<AnimateSVGPathValuesComponent
  mode="AnimatedPathProps"
  config={{ type: 'spring', useNativeDriver: true }}
  pathProps={{ d: PATH, stroke: '#fff' }}
  animatedPathProps={[
    { name: 'strokeWidth', from: 1, to: [4, 2] },
    {
      name: 'fillOpacity',
      from: 0,
      to: [1],
      config: { type: 'timing', duration: 300, useNativeDriver: true },
    },
  ]}
  isSequence
  autoStart
/>

Both render AnimatedPath (Animated.createAnimatedComponent(Path), also exported) and expose { start, stop, reset, reverse } plus value / values through ref.


SVG

  • AbsoluteLinearGradient — absolutely positioned full‑size SVG gradient. Props: colors: string[], opacities? (default [1, 1]), start? (default {x:'0',y:'0'}), end? (default {x:'1',y:'0'}), style?. Stops are evenly spaced.
  • LinearGradient — a View with position: 'relative' containing AbsoluteLinearGradient and your children. Same props plus style.

Hooks

useDeviceOrientation

Subscribes to Dimensions changes and returns { screenWidth, screenHeight, orientation, relativeX, relativeY, relativeShort, relativeLong, relativeXWorklet, relativeYWorklet, relativeShortWorklet, relativeLongWorklet, normalize, normalizeShort }. Use this (not the static functions) when values must react to rotation or inside Reanimated worklets.

useFontSizes(fontScale = 1)

Returns the FontSize → number map computed with the reactive normalize.

useViewDimensions()

const [layout, onLayout] = useViewDimensions(); — layout is LayoutRectangle | null.

useImageSize / useImageSizes / calculateSize

Fit remote images into maxWidth × maxHeight preserving aspect ratio.

  • useImageSize({ image, maxWidth, maxHeight }) → { imageSize: { width, height }, calculateSize }
  • useImageSizes({ images, maxWidth, maxHeight }) → { imageSizes, calculateSize } (sequential Image.getSize)
  • calculateSize({ imageWidth, imageHeight, maxWidth, maxHeight }) → { displayWidth, displayHeight }

useInputRef({ inputValidationFunction? })

Returns [inputRef, onChangeText, inputValue, defaultInputValue]. Keeps the current text on inputRef.current.value without re‑rendering; when a validator rejects input, the hook switches the field to controlled mode so the rejected text is reverted. Wire all four returned values when using a validator.

useKeyboardListeners({ listeners, keyboardHeightRef?, subscribeCondition? })

Registers the given Keyboard listeners (keyboardDidShow, keyboardWillHide, ...) and tracks the keyboard height in a ref. Returns { keyboardHeightRef }.

useScrollableItems({ itemsFetchingFunction, limit?, minFetchDuration?, fetchCooldown?, itemsFetchingFunctionArgs? })

Pagination state for FlatList‑style lists.

const list = useScrollableItems<Post, { userId: string }>({
  limit: 20,
  itemsFetchingFunction: ({ limit, skip, userId }) =>
    api.posts({ limit, skip, userId }),
  itemsFetchingFunctionArgs: { userId },
});
<FlatList
  data={list.items}
  onLayout={list.onLayout}
  onEndReached={list.onItemsEndReached}
  refreshing={list.refreshingItems}
  onRefresh={list.onRefreshItems}
/>;

Returns { items, setItems, onLayout, onItemsEndReached, onRefreshItems, refreshingItems, loading, setLoading, updateListItem(id, key, item), removeListItem(id, key), fetchItems({ refresh? }) }. Fetching stops once a page returns fewer than limit items.

useDebounce({ delayInMilliSecs, onTrigger, onPreTrigger?, onPostTrigger?, onDebounceInvocation? })

Returns { debounce(overrides?), cancelDebounce }. Each debounce() call restarts a Scheduler.Timer; overrides can replace the delay and callbacks for that invocation. Default delay 5000ms if 0/falsy.

useInterval({ intervalInMilliSecs, onTrigger, autoStart?, onPreTrigger?, onPostTrigger?, onIntervalInvocation? })

Returns { startInterval(overrides?), stopInterval }. Cleans up on unmount.

useTimeout({ cb, ms }, deps)

setTimeout that resets whenever deps change and clears on unmount.

useTimer({ seconds, onTimerEnds, start })

Countdown; returns { minutes, seconds } and calls onTimerEnds at zero.

useTranslation({ text })

Returns the translated string (initially text) using LocalizationProvider. Logs an error if the provider is missing.

useAnimatedStringValue(inputRange, outputRange, config?)

Creates a stable RN Animated.Value and returns { value, interpolatedValue } where interpolatedValue maps numbers to strings (colours, degrees, ...).

useIsolateObservables / useIsolateRef

See Render isolation.


Storage (MMKV)

All accessors share one MMKV instance (storageAccessorsInstance, id 'rne-csa').

createStorageAccessors(key)

const settings = createStorageAccessors<{ theme: 'dark' | 'light' }>(
  'settings'
);
settings.store({ theme: 'dark' });
settings.retrieve(); // { theme: 'dark' } | und