Help us improve
Share bugs, ideas, or general feedback.
From expo-rn-plugin
Load coding standards and conventions for this React Native / Expo project. Use when you need guidance on TypeScript patterns, Tamagui tokens, Zustand stores, Lingui i18n, Doppler env vars, or Zustand state ownership rules.
npx claudepluginhub ksairi-org/claude --plugin expo-rn-pluginHow this skill is triggered — by the user, by Claude, or both
Slash command
/expo-rn-plugin:coding-standardsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Apply the following standards to all code in this project.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
Share bugs, ideas, or general feedback.
Apply the following standards to all code in this project.
If this project uses
@ksairi-org/*libraries, run/expo-rn-plugin:libsbefore writing any utility, hook, or layout code — those packages replace many standard alternatives.
When a pattern isn't covered by these standards, look at ksairi-org/virtual-wallet — the canonical production app built on this exact stack. Use it to answer "how was X solved in practice?" before inventing a new approach.
any — use proper types, generics, or type guardsas assertions — fix types at the sourcetsc --noEmit and fix all errors (zero errors is a baseline)eslint-disable-next-line react-hooks/exhaustive-deps — fix the dependency issueAlways resolve UI needs from the highest available source before reaching for lower ones:
@atoms, @molecules, @organisms)@ksairi-org/ — shared org packages; covers buttons, touchables, images, screen containers, forms, authXStack, YStack, Text, Spinner, Stack, …react-native — only when no Tamagui or @ksairi-org/ equivalent existsNever import View, Text, TouchableOpacity, Pressable, or Image from react-native when a Tamagui or @ksairi-org/ wrapper covers the use case.
@ksairi-org/Before adding a new component to the project-local layers, ask: would any other app on this stack benefit from this? If yes, and it has no app-specific tokens, data, or business logic, push it to @ksairi-org/libs instead and consume it remotely. Examples that belong upstream: generic wrappers around third-party primitives (KeyboardScrollView), shared layout primitives, utility hooks. This rule only applies when you are a member of the ksairi-org GitHub org and the consuming project already uses @ksairi-org/* packages.
Organize project-local shared components into three layers and place new components in the correct one:
@ksairi-org/ui-containers)Every screen must use Containers.Screen as its root element. It handles safe area insets automatically (via useSafeAreaInsets) so you never need SafeAreaView directly. react-navigation adjusts the inset context per navigator, so the all-edges default is self-correcting — no double-padding inside a Stack with a header or inside a Tabs screen.
import { Containers } from '@ksairi-org/ui-containers'
// Screen with its own ScrollView/KeyboardScrollView
<Containers.Screen shouldAutoResize={false}>
<KeyboardScrollView>…</KeyboardScrollView>
</Containers.Screen>
// Screen without scroll — auto-resize switches to ScrollView if content overflows
<Containers.Screen>
<Containers.SubY>…</Containers.SubY>
</Containers.Screen>
Containers.Screen — outermost; handles safe area, auto-resize to ScrollView when content overflowsContainers.SubY — vertical sub-section with standard horizontal padding ($md)Containers.SubX — horizontal sub-section with standard horizontal padding ($md)edges prop (default all four) — override only when you need to exclude specific edgesshouldAutoResize={false} — required when the screen already contains its own ScrollViewButtons specifically — @ksairi-org/ui-button takes priority over Tamagui's Button. Never use Tamagui's raw Button or react-native touchables for interactive buttons:
CTAButton (has loading prop and spinnerColor; pass backgroundColor from your theme; spinner is Tamagui Spinner)BasicButton (full ButtonProps pass-through, opacity=0.4 when disabled)GhostButton (transparent background, opacity=0.4 when disabled; pass color from your theme)IconButton (circular, requires icon: ReactNode, full ButtonProps pass-through)BaseButton (accepts leftIcon/rightIcon)SizingAnimatedButton from @ksairi-org/ui-button-animated (backgroundColor required; measures its own width internally)AnimatedButton from @ksairi-org/ui-button-animated (backgroundColor and width: number both required; prefer SizingAnimatedButton unless you need explicit width control)Trans + t for every hardcoded user-visible stringt`…` for prop strings (placeholders, aria labels, alert titles)Trans, useLingui from @lingui/react/macroimport statement per module path (prevents import/no-duplicates)src/theme/tamagui.config.ts; themes: src/theme/themes.ts$surface-app, $text-primary — always check themes.ts before usingallowedStyleValues: "strict" — only token values; raw hex/rgba will error at compile time$sm, $md, $lg from sizesSpaces; radius from radius tokenssrc/components/ — no raw <Text> with style propsStyleSheet.create() — use Tamagui styled()styled() from @tamagui/core first, then use token-based style props on the wrapper| Layer | Owner |
|---|---|
| Server state | react-query / orval hooks |
| Client/UI state | Zustand |
If data comes from the backend it belongs in react-query. Zustand stores should be thin.
type MyStore = MyStoreState & MyStoreFunctions;
const INITIAL_STATE: MyStoreState = { ... };
const useMyStore = create<MyStore>()(
persist(
(set) => ({
...INITIAL_STATE,
setKeyValue: (key, value) => set((state) => ({ ...state, [key]: value })),
}),
{ name: "my-storage", storage: createJSONStorage(() => createZustandMmkvStorage({ id: "my-storage" })) },
),
);
// Good
const firstName = useUserStore((state) => state.firstName);
// Bad — re-renders on any store change
const store = useUserStore();
mobile, configs dev / stg / prdenv.template.yaml: EXPO_PUBLIC_FOO={{ .FOO }} (left = shell var, right = Doppler key)doppler secrets set FOO="value" --project mobile --config stgyarn sync-env-varsEXPO_PUBLIC_ on the Doppler key — that prefix belongs only on the left side of env.template.yaml. Example: Doppler key is SUPABASE_API_KEY; template maps it as EXPO_PUBLIC_SUPABASE_API_KEY={{ .SUPABASE_API_KEY }}.axios directly in componentsaxios only for non-REST endpoints or one-off authenticated file uploads@dev-plugins/react-query) before touching codeFlashList from @shopify/flash-list — never FlatListestimatedItemSize is required — omitting it causes a warning and degrades performancedate-fns — always pass the locale from expo-localization for locale-aware outputDate.toLocaleDateString() — output varies by device locale settingsjest-expo; render helper: @testing-library/react-nativerenderWithProviders helper that includes Tamagui, query client, and i18n providersscreen.getByText, screen.getByRole) — never on internal state/expo-rn-plugin:testing for canonical test patterns and provider setup