StatsHub Docs
Packages

ui-native

The React Native component kit — deep imports, no barrel, and the three seams it deliberately leaves to the app.

@statshub/ui-native is the React Native component kit that expo renders with. It was lifted out of the app, where it had lived as an ordinary directory of components, and the app now reaches it by name across roughly 550 call sites.

Importing

Deep paths, always.

import { Button } from "@statshub/ui-native/button/button";
import { useSurfaceStyle } from "@statshub/ui-native/sheet/surface-style";

There is no barrel and no main, deliberately. A root index.ts re-exporting 140 modules is one import away from pulling the whole kit into a bundle that wanted one button, and Metro does not tree-shake its way back out.

Why the files sit at the package root

Metro's package-exports wildcards must name an exact extension. A "./*": "./src/*" map does not resolve, and an array fallback resolves for tsc but not for Metro. This kit mixes .ts and .tsx, so it ships no exports field at all and lets Metro's ordinary file lookup work — which is also why the component directories are at the package root rather than under src/.

button.tsx
package.json — no main, no exports

Note the absence of src/ and of an index.ts. Both are consequences of the box above, not oversights.

What it must not know

The kit renders. It does not decide where a failure is reported or how the app talks to disk. Three seams exist because of that, and each has an app-side half:

SeamThe kit ownsThe app owns
ErrorsNothingRootErrorBoundary around UIProvider, AppErrorBoundary as its children
Surface styleThe store (sheet/surface-style)Reading it at launch, writing each change
Route presentationThe RoutePresentation vocabularyWhich route gets which presentation

A kit file that needs @/lib/... is the smell: either the thing it needs belongs in the kit too, or the component belongs in the app. Both answers are fine; a package importing from an app is not.

Data lifecycle

DataList, ListScreen, and SearchScreen require an explicit loading contract. Prefer loadPhase="loading" | "error" | "settled"; the required legacy isPending form remains available while callers migrate. The shared resolver in state/content-state turns settled plus zero rows into empty. An unresolved request can therefore never fall through to an empty screen just because its data value is still the default empty array.

For a screen composed from multiple requests, combine only its active required sources with combineContentLoadPhases. A disabled optional request is not a requirement. Cached content should stay settled during a background refresh or failure so usable rows are not replaced by a spinner.

Theme

_core/_theme holds the uniwind CSS entry and the generated .d.ts. apps/expo/metro.config.js addresses both by path from the workspace root, and the app's tsconfig.json names the .d.ts explicitly — without that, every className in the app stops typechecking.

See also platform differences and add a UI component.

On this page