Production-ready Atomic Design core for Expo / React Native.
Copy once, use everywhere — a project-agnostic UI foundation built with NativeWind v4, TanStack Query v5, Zustand v5, and Zod v4.
native-atomic is a reusable core layer for Expo projects that gives you:
| Layer | What you get |
|---|---|
| Atoms | Button, Text, Input, Badge, Chip, Avatar, Icon, Divider, Skeleton |
| Molecules | InputField, SearchBar, RatingRow, PriceDisplay, StatCard, IconButton |
| Hooks | useDebounce, useThrottle, useKeyboard, useBoolean, useAppState, useMount, usePrevious, useUnmount |
| Utils | formatPrice, formatDate, cn, truncate, slugify, initials, groupBy, uniqueBy, isEmail, isPhone |
| Lib | Axios instance (JWT refresh, 401/422 handling), TanStack Query client, MMKV storage, typed env |
| Auth store | Zustand + MMKV persist slice with role-based state |
Everything is TypeScript-strict, zero any, Zod-validated at API boundaries, and has ≥ 95% test coverage on all atoms and utilities.
| Tool | Version | Role |
|---|---|---|
| Expo | ~56 | Build & bundler |
| React Native | 0.85 | Runtime |
| Expo Router | ~56.2 | File-based navigation |
| NativeWind | ^4.2 | Tailwind CSS for RN |
| TanStack Query | ^5 | Server state |
| Zustand | ^5 | Global UI state |
| Zod | ^4 | Schema validation |
| React Hook Form | ^7 | Form state |
| MMKV | ^4 | Persistent storage |
| Reanimated | 4.3 | Animations |
| TypeScript | ~6 | Type safety |
| Jest | ^29 | Testing |
git clone https://github.com/tekin-labs/native-atomic.git my-app
cd my-app
npm installcp .env.example .env
# Set EXPO_PUBLIC_API_URL and EXPO_PUBLIC_APP_NAMEnpx expo startIf you already have an Expo project, copy just the relevant folders:
src/core/ ← Atoms, molecules, hooks, utils, theme
src/lib/ ← API client, query client, MMKV storage
src/store/ ← Zustand root store + middleware
Then wire up the path alias in tsconfig.json:
And wrap your root layout:
// app/_layout.tsx
import { AppProviders } from '@/core/components/templates'
export default function RootLayout() {
return (
<AppProviders>
<Stack screenOptions={{ headerShown: false }} />
</AppProviders>
)
}import { Button, Text, Input, Badge, Avatar, Icon } from '@/core/components/atoms'
import { InputField, SearchBar, RatingRow, PriceDisplay, StatCard } from '@/core/components/molecules'
// Button variants: primary | secondary | ghost | danger | accent | link
<Button label="Devam Et" onPress={fn} variant="primary" size="md" />
// Text variants: display | heading | subheading | body | caption | label | overline
<Text variant="heading" weight="bold">Başlık</Text>
// Rating with interaction
<RatingRow rating={4.5} reviewCount={128} interactive onRatingChange={setRating} />
// Price with discount badge
<PriceDisplay amount={750} currency="TRY" originalAmount={1000} />import { atoms, molecules, hooks, utils } from '@/core'
// Everything namespaced — great for barrel-free tree shaking
const { Button, Text } = atoms
const { useDebounce } = hooks
const { formatPrice, cn } = utilsimport { useDebounce, useKeyboard, useBoolean, useThrottle } from '@/core/hooks'
const query = useDebounce(searchText, 300) // debounced value
const { isVisible } = useKeyboard() // keyboard state
const modal = useBoolean() // { value, toggle, setTrue, setFalse }
const throttledSave = useThrottle(save, 1000) // throttled functionimport { formatPrice, formatDate, truncate, initials, cn } from '@/core/utils'
formatPrice(1500) // "₺1.500"
formatDate(new Date(), 'relative') // "3 dakika önce"
truncate('Uzun bir başlık', 20) // "Uzun bir başlık..."
initials('Ayşe Kaya') // "AK"
cn('px-4', isActive && 'bg-iris-50') // conditional className mergeAll design tokens live in tailwind.config.js. Override the brand colors for your project:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
// Replace the primary brand color
iris: {
50: '#F0EEFF',
500: '#YOUR_BRAND_PRIMARY',
600: '#YOUR_BRAND_DARK',
},
// Surface and semantic colors
'surface-base': '#FFFFFF',
'surface-raised': '#F9F9FC',
},
fontFamily: {
display: ['YourDisplayFont_700Bold'],
body: ['YourBodyFont_400Regular'],
},
},
},
}No component changes required — everything references tokens by name.
src/
app/ ← Expo Router routes
(auth)/ ← Unauthenticated routes
(tabs)/ ← Main tab navigation
core/ ← ★ Reusable, project-agnostic layer
components/
atoms/ ← Indivisible primitives
molecules/ ← Composed UI blocks (2–5 atoms)
organisms/ ← Complex, domain-aware UI
templates/ ← Layout skeletons + providers
hooks/ ← Universal utility hooks
utils/ ← Pure functions
theme/ ← Design token references
domains/ ← Business logic per domain
auth/
expert/
client/
offer/
match/
payment/
assessment/
lib/ ← Axios, TanStack Query, MMKV
store/ ← Zustand root store
Atom → can import nothing from core
Molecule → can import Atoms only
Organism → can import Atoms + Molecules
Template → can import all three
Reverse imports (Atom → Molecule) are banned and caught by ESLint.
| Decision | Why |
|---|---|
| TanStack Query for server state | Automatic caching, background refetch, stale-while-revalidate |
| Zustand + MMKV for global/persistent state | Tiny bundle, synchronous MMKV reads, no boilerplate |
| Zod at API boundaries | Runtime safety — bad API responses fail loudly instead of silently corrupting state |
| NativeWind v4 over StyleSheet | Consistent design system, no style object sprawl, IDE autocomplete |
| Expo Router | File-based routing = zero manual stack configuration |
No React.FC |
Better inference, props type exported separately for extensibility |
# Development
npx expo start
# Tests (all)
npx jest --coverage
# Tests (watch)
npx jest --watch
# Type check
npx tsc --noEmit --skipLibCheck
# Lint (zero warnings policy)
npx eslint src --max-warnings 0
# Format check
npx prettier --check src
# Format fix
npx prettier --write src
# Circular dependency check
npx madge --circular --extensions ts,tsx src/core/index.ts| Scope | Requirement |
|---|---|
core/components/atoms/ |
≥ 95% lines |
core/utils/ |
100% lines |
core/hooks/ |
≥ 90% lines |
src/core/components/atoms/MyAtom/MyAtom.tsxsrc/core/components/atoms/MyAtom/MyAtom.test.tsx→ 100% line coveragesrc/core/components/atoms/MyAtom/index.ts- Add
export * from './MyAtom'toatoms/index.ts - Add named export to
components/index.ts
- Same folder structure under
molecules/ - Import from
@/core/components/atomsonly — never from other molecules - 95%+ line coverage required
- Update
molecules/index.tsandcomponents/index.ts
Domain-specific? If the component needs API data or business logic → it belongs in
domains/[x]/components/instead.
// ❌ Reverse atomic import
import { InputField } from '../molecules/InputField' // inside an Atom — BANNED
// ❌ Server state in useState
const [data, setData] = useState([])
useEffect(() => { fetchData().then(setData) }, [])
// ✅ Use TanStack Query
// ❌ Business logic in a component
function ExpertCard({ id }) {
const [price, setPrice] = useState(0) // BANNED
}
// ✅ const { price } = useExpertPrice(id)
// ❌ Manual type instead of Zod inference
type Expert = { id: string; name: string } // BANNED
// ✅ export type Expert = z.infer<typeof ExpertSchema>
// ❌ Unsafe API cast
const expert = response.data as Expert // BANNED
// ✅ const expert = ExpertSchema.parse(response.data)
// ❌ StyleSheet or inline style (when a token exists)
<View style={{ padding: 16, backgroundColor: '#5C4FD6' }} />
// ✅ <View className="p-4 bg-iris-500" />MIT © Tekin Labs
Built with ♥ using Expo SDK 56 · TypeScript 6 · NativeWind v4 · Tekin Labs
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }