- git clone https://github.com/aianov/react-native-mobx-template
bun i(oryarnornpm i) [bun prefered]npx expo start
src/
βββ __tests__/ # For components, functions, screenshot and etc all type of tests [I use Jest btw]
βββ app/ # Have `main.tsx`, and `App.tsx`. Also folders like `layouts` and `router`.
βββ assets/ # Inside this folder we have 6 folders with animations, fonts, icons, images, sounds, and global styles from StyleSheet
βββ core/ # One of the important folder here. Inside we have folders like: api, config, hooks, lib, locales and etc We will talk about this folder later.
βββ modules/ # Now I'll start to show you unique architecture.
Some kind of folders are too easy and small to explain, so I will skip folders sometime
This is where magic happens. Let me show you structure first:
core/
βββ api/ # API configuration
βββ config/ # App constants, types, regex, functions
βββ hooks/ # Custom React hooks (global)
βββ lib/ # π₯ The most important - all utilities
βββ locales/ # i18n translations (en, ru)
βββ storage/ # AsyncStorage wrappers
βββ stores/ # MobX global stores
βββ ui/ # π¨ Reusable UI components
βββ utils/ # Small utility functions
βββ widgets/ # Complex reusable widgets
Let's go through each one...
api/
βββ api.ts # HTTP instance configuration
Here we configure our HTTP client. Base URL, interceptors, headers - everything. Using our own axios like function, which helps us to use mobxSaiFetch function with DebuggerUi [We'll talk about this later]
config/
βββ constants.ts # App-wide constants
βββ functions.ts # Helper functions
βββ regex.ts # Regex patterns
βββ types.ts # Global TypeScript types
All your app configuration in one place. Constants like API endpoints, regex for validation, global types. Base show, u can do whatever you want here
This is where all the magic utilities live:
lib/
βββ arr/ # Array utilities (empty, ready for use)
βββ date/ # Date formatting functions
βββ debuggerUi/ # π₯ Built-in debugger UI component
βββ global/ # Global extensions (Array.prototype, etc.)
βββ helpers/ # General helper functions
βββ mobx-toolbox/ # π₯ MobX utilities (THE CORE)
β βββ mobxDebouncer/ # I think the best Debouncer ever written on MobX to any actions
β βββ mobxSaiFetch/ # HTTP requests with MobX (like React Query but much better)
β βββ mobxState/ # Easy state creation
β βββ mobxValidator/ # Form validation
β βββ useMobxForm/ # Form management
β βββ useMobxUpdate/ # State updates helper [U'll never will use it because its in mobxSaiFetch function inside]
βββ navigation/ # Navigation utilities and hooks
βββ notifier/ # Toast notifications system
βββ numbers/ # Number formatting
βββ obj/ # Object utilities
βββ performance/ # Performance hooks (debounce, optimized callbacks)
βββ string/ # String utilities
βββ text/ # Text formatting and components
βββ theme/ # Theme utilities (colors, gradients)
mobxSaiFetch- like React Query but for MobX. Caching, optimistic updates, infinite scroll - everything!mobxState- create MobX state in one linemobxValidator- validation schemas like Zod but simpleruseMobxForm- form management with validationuseMobxUpdate- update nested state easily
locales/
βββ en/
β βββ translation.json
βββ ru/
βββ translation.json
i18n translations. Just add new language folder and translation.json file.
storage/
βββ AppStorage.ts # App-specific storage
βββ CacheManager.ts # Cache management
βββ index.ts # Main export
βββ types.ts # Storage types
AsyncStorage wrappers. Easy to use, type-safe.
stores/
βββ global-interactions/ # Global app interactions
β βββ global-interactions/
β βββ route-interactions/
βββ memory/ # Memory management
βββ memory-interactions/
βββ memory-services/
Global MobX stores. Things that need to be accessed from anywhere.
Holy... we have a lot here:
ui/
βββ AnimatedTabs/ # Animated tab component
βββ AnimatedTransition/ # Page transitions
βββ AsyncDataRender/ # Render based on async state
βββ BgWrapperUi/ # Background wrapper
βββ BlurUi/ # Blur effect
βββ BottomSheetUi/ # Bottom sheet modal
βββ BoxUi/ # Flexbox wrapper (like Box in MUI)
βββ ButtonUi/ # Button component
βββ CheckboxUi/ # Checkbox
βββ CleverImage/ # Smart image with caching
βββ ContextMenuUi/ # Context menu
βββ CustomRefreshControl/ # Pull to refresh
βββ DatePickerUi/ # Date picker
βββ ErrorTextUi/ # Error text display
βββ FormattedText/ # Text with formatting
βββ GridContentUi/ # Grid layout
βββ GroupedBtns/ # Button group
βββ HoldContextMenuUi/ # Long press context menu
βββ ImageSwiper/ # Image carousel
βββ InputUi/ # Text input
βββ LiveTimeAgo/ # "5 min ago" component
βββ LoaderUi/ # Loading spinner
βββ MainText/ # Main text component
βββ MediaPickerUi/ # Image/video picker
βββ Modal/ # Modal component
βββ ModalUi/ # Another modal variant
βββ PageHeaderUi/ # Page header
βββ PhoneInputUi/ # Phone number input
βββ PressableUi/ # Pressable wrapper
βββ RefreshControlUi/ # Refresh control
βββ SecondaryText/ # Secondary text
βββ SelectImageUi/ # Image selector
βββ Separator/ # Divider line
βββ SimpleButtonUi/ # Simple button
βββ SimpleInputUi/ # Simple input
βββ SimpleModalUi/ # Simple modal
βββ SimpleTextAreaUi/ # Simple textarea
βββ SkeletonUi/ # Skeleton loading
βββ SwitchUi/ # Toggle switch
βββ TextAreaUi/ # Textarea
βββ index.ts # All exports
βββ types.ts # UI types
Every component you need is here. All themed, all customizable from src/modules/theme/stores/theme-interactions.
utils/
βββ device-info.ts # Device information
βββ haptics.ts # Haptic feedback
βββ jwt.ts # JWT utilities
βββ notifications.ts # Push notifications
Small utility functions. Nothing fancy, just useful stuff.
widgets/
βββ wrappers/
βββ MainWrapper/ # Main app wrapper
Complex reusable widgets. Wrappers, compound components, etc.
modules/
βββ auth/ # Authentication module
β βββ pages/ # Auth screens
β βββ shared/ # Shared auth components
β βββ stores/ # Auth MobX stores
β βββ widgets/ # Auth widgets
βββ onboarding/ # Onboarding module
β βββ pages/
β βββ shared/
β βββ stores/
βββ theme/ # Theme module
βββ stores/ # Theme MobX store
pages/- screens/pagesshared/- shared components for this modulestores/- MobX stores for this module in S.A.I Architecturewidgets/- complex widgets for this module
This is Feature-Sliced Design but simpler. Each feature is isolated. Easy to understand, easy to maintain.
auth/
βββ stores/ # Authentication module
β βββ auth-actions/ # Actions store - only requests function and response states [mobxSaiFetch function here]
β βββ auth-interactions/ # Interactions store - All interaction logic with JSX
β βββ auth-service/ # Services store - Boilerplate from interactions and actions, etc: success & error handlers for action store
β β
β βββ index.ts/ # Re-export for best path-alias experience and clean code
This is probably one of the coolest features you've ever seen. A floating draggable debug panel that shows everything happening in your app in real-time.
A small floating React icon button that you can drag anywhere on screen. Tap it to open the full this debug panel:
Shows all HTTP requests with:
- Request/Response data with syntax highlighting
- CACHED tag (yellow border) - data from local memory cache
- LOCAL-CACHED tag (purple border) - data from localStorage
- NO-PENDING tag - request made without loading state
- FORCE-FETCH tag - forced fresh request
- Repeat count (Γ3 means same request was made 3 times)
- Copy button for each request
Shows current in-memory cache:
- All cached entries with their keys
- Data preview
- Delete individual cache items
- Clear all cache
Real-time logs with colors:
- Info (blue)
- Success (green)
- Warning (orange)
- Error (red)
- Copy last 100 logs button
- Or press to any log to copy one
- Auto-scroll to bottom
Shows all AsyncStorage data:
- Key-value pairs
- Array length indicators
- Delete individual items
Shows cached images from storage
Shows history of all cache mutations:
saiUpdater- in-memory updatessaiLocalCacheUpdater- local cache updatessaiLocalStorageUpdater- localStorage updates- Shows what changed (added/removed items, changed keys)
Search across ALL tabs at once! Find any string in:
- Request URLs
- Request/Response bodies
- Cache data
- LocalStorage
- Navigate between matches
Each tab has +/- buttons to adjust font size. Saved to localStorage! [Press DEFF to return default font size]
// In your App.tsx or root component
import { DebuggerUi } from '@lib/debuggerUi/DebuggerUi';
export const AppContent = () => {
return (
<>
{__DEV__ && <DebuggerUi />} {/* Only show in development */}
</>
);
};
export const App = () => {
return (
<AppContent />
)
}That's it! Now you have full visibility into your app's HTTP layer π₯
This is the heart of the template. Like React Query, but for MobX. Actually, I think it's even better.
// In your store
class UserActionsStore {
constructor() { makeAutoObservable(this); }
profile: MobxSaiFetchInstance<GetProfileResponse> = {}
getProfileAction = () => {
profile = mobxSaiFetch(
`/user/profile/${userId}`, // URL
null, // Body {} (null for GET)
{
id: 'getUserProfile', // Cache key
storageCache: true, // Save to AsyncStorage
onSuccess: getProfileSuccessHandler, // Success callback
onError: getProfileErrorHandler // Error callback
}
);
}
}import { observer } from 'mobx-react-lite';
import { AsyncDataRender } from '@core/ui';
export const ProfileScreen = observer(() => {
const {
profile: { status, data }
} = userStore;
return (
<AsyncDataRender
status={status}
data={data}
emptyComponent={<ProfileEmpty />} // U can customize or make by default in AsyncDataRender core/ui
errorComponent={<ProfileError />} // On error component fallback
refreshControllCallback={onRefresh}
renderContent={() => {
return <ProfileCard data={profile.data} />
}
/>
);
});interface MobxSaiFetchInstance<T> {
// Data
data: T | null;
error: Error | null;
body: any;
// Main status
status: "pending" | "fulfilled" | "rejected";
isPending: boolean;
isFulfilled: boolean;
isRejected: boolean;
// Scope status (for infinite scroll)
scopeStatus: "pending" | "fulfilled" | "rejected" | "";
isScopePending: boolean;
isScopeFulfilled: boolean;
isScopeRejected: boolean;
// Top/Bottom loading (infinite scroll)
isTopPending: boolean;
isBotPending: boolean;
isHaveMoreTop: { isHaveMoreTop: boolean };
isHaveMoreBot: { isHaveMoreBot: boolean };
// Methods
fetch: (promise) => this;
reset: () => this;
saiUpdater: (...) => void; // its basically useMobxUpdate instance (Can update cache too, for sync with local data)
}mobxSaiFetch(url, body, {
// Required
id: 'uniqueCacheKey', // Cache identifier
// HTTP
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
headers: { 'X-Custom': 'value' },
timeout: 5000,
// Caching
storageCache: true, // Persist to AsyncStorage
takeCachePriority: 'localStorage', // 'localStorage' | 'localCache'
// Behavior
fetchIfPending: false, // Skip if already loading
fetchIfHaveData: true, // Re-fetch even if data exists
needPending: true, // Show loading state
shadowFirstRequest: true, // First request updates cache silently
// Data extraction
takePath: 'data.user', // Extract nested data
pathToArray: 'items', // Path to array for updates
// Callbacks
onSuccess: (data, body) => {},
onError: (error) => {},
onCacheUsed: (data, body, priority) => {},
// Infinite Scroll
dataScope: {
scrollRef: flatListRef, // For React Native
topPercentage: 20, // Trigger top fetch at 20%
botPercentage: 80, // Trigger bottom fetch at 80%
startFrom: 'top' | 'bot', // Initial position
relativeParamsKey: 'cursor', // Param for pagination
isHaveMoreResKey: 'hasMore', // Response field for more data
setParams: setParams, // State setter for params
scopeLimit: 100, // Max items in memory
},
// Add fetched data to array
fetchAddTo: {
path: 'messages', // Array path
addTo: 'start' | 'end', // Where to add new data
},
// Optimistic Updates
optimisticUpdate: {
enabled: true,
createTempData: (body) => ({
id: `temp_${Date.now()}`,
...body,
isTemp: true,
}),
targetCacheId: 'getMessages',
}
});! To use mobxSaiFetch with "baseUrl" setted. You need to use createMobxSaiHttpInstance. !
import { createMobxSaiHttpInstance } from '@lib/mobx-toolbox';
import { Platform } from 'react-native';
export const createInstance = () => {
const mobxHttpInstance = createMobxSaiHttpInstance({
baseURL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
});
mobxHttpInstance.defaults.withCredentials = true;
mobxHttpInstance.interceptors.request.use(
async (config) => {
config.withCredentials = true; // ALL REQUESTS
return config;
},
(error: any) => {
console.error(error) // ALL ERRORS
return Promise.reject(error);
}
);
mobxHttpInstance.interceptors.response.use(
async (response) => {
console.log(response); // ALL RESPONSES
return response;
},
(error: any) => {
console.error(error); // ALL ERRORS FROM RESPONSES
return Promise.reject(error);
}
);
return mobxHttpInstance;
};const { messages } = messageActionsStore
// Update single item in array
messages.saiUpdater(
'message-123', // ID of item to update
'isRead', // Field to update [TYPE SAFE]
true, // New value
'id', // ID field name
'getMessages', // Cache ID
'both' // Update both caches
);
// Update with function
messagesStore.messages.saiUpdater(
'message-123',
'likes',
(prev) => prev + 1,
'id',
'getMessages',
'localStorage'
);
// Update entire array
messagesStore.messages.saiUpdater(
null, // null = update array
null,
(prevArray) => prevArray.filter(m => !m.isDeleted),
'id',
'getMessages',
'both'
);import {
saiLocalCacheUpdater,
saiLocalStorageUpdater,
saiCacheUpdater
} from '@lib/mobx-toolbox';
// Update in-memory cache
await saiLocalCacheUpdater('getMessages', (currentData) => {
return {
...currentData,
messages: currentData.messages.filter(m => m.id !== deletedId)
};
});
// Update localStorage
await saiLocalStorageUpdater('getMessages', (currentData) => {
return { ...currentData, unreadCount: 0 };
});
// Update both at once
await saiCacheUpdater('getMessages', (currentData) => {
return { ...currentData, lastSeen: Date.now() };
});// In getMessagesAction function:
const MESSAGES_LIMIT = 50
params = mobxState({
chat_id: "...",
relative_id: null,
up: true,
limit: MESSAGES_LIMIT
})("params")
messages = mobxSaiFetch(
'/chat/messages',
params.params,
{
id: 'getChatMessages',
pathToArray: 'messages',
takeCachePriority: "localStorage",
method: 'GET',
needPending,
fetchIfPending: false,
fetchIfHaveData: false,
fetchIfHaveLocalStorage: false,
storageCache: true,
onSuccess: getMessagesSuccessHandler,
onError: getMessagesErrorHandler,
maxCacheData: 10,
dataScope: {
startFrom: "bot", // Start from bottom (newest)
scrollRef: messagesScrollRef,
topPercentage: 80, // Load older when scroll 15% from top
botPercentage: 20, // Load newer when scroll 85% from top
setParams: params.setParams,
relativeParamsKey: "relative_id", // path to "relative_id" key from params to auto-reload for auto fetches in virtual list
upOrDownParamsKey: "up", // path to "up" key from params
isHaveMoreResKey: "is_have_more", // path to "is_have_more" key from backend response
howMuchGettedToTop: 2, // How many pages can load up before scopeLimit start works
upStrategy: "reversed",
scopeLimit: MESSAGES_LIMIT * 2 // Keep max 100 messages in memory
},
cacheSystem: {
limit: MESSAGES_LIMIT
},
fetchAddTo: {
path: "messages",
addTo: "start"
},
}
);import { LegendList, LegendListRef } from '@legendapp/list';
export const ChatScreen = observer(() => {
const { messages } = messageActionsStore;
const { messagesScrollRef: { setMessagesScrollRef } } = messageInteractionsStore;
const scrollRef = useRef<LegendListRef | null>(null);
useEffect(() => {
if (!scrollRef.current) return
setMessagesScrollRef(scrollRef as any);
}, [scrollRef.current]);
return (
<LegendList
ref={scrollRef}
data={processedMessages}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={contentContainerStyle}
maintainVisibleContentPosition={true}
recycleItems={false}
drawDistance={500}
estimatedItemSize={100}
getEstimatedItemSize={getEstimatedItemSize}
stickyIndices={Platform.OS === 'ios' ? stickyHeaderIndices : undefined}
viewabilityConfig={viewabilityConfig}
onScroll={handleScrollInternal}
onMomentumScrollBegin={handleMomentumScrollBegin}
scrollEventThrottle={16}
keyboardShouldPersistTaps='handled'
keyboardDismissMode='interactive'
bounces={true}
/>
);
});import { hasSaiCache } from '@lib/mobx-toolbox';
// Check if data exists in any cache
const hasCache = await hasSaiCache('all', 'getUserProfile'); // Usefull for needPending option
// Check specific cache types
const hasLocalCache = await hasSaiCache(['localCache'], 'getUserProfile');
const hasStorage = await hasSaiCache(['localStorage'], 'getUserProfile');
const hasData = await hasSaiCache(['data'], userStore.profile);Full theming support with MobX reactivity. Change theme - UI updates instantly.
// All available theme tokens
interface ThemeT {
// Backgrounds
bg_000: string; // Lightest
bg_100: string;
bg_200: string;
bg_300: string;
bg_400: string;
bg_500: string;
bg_600: string; // Darkest (no really always)
// Borders (converted from CSS to RN format)
border_100: string;
border_200: string;
// ...
// Border radius (numbers for RN)
radius_100: number; // 20
radius_200: number; // 15
// ...
// Button backgrounds
btn_bg_000: string;
btn_bg_100: string;
// ...
// Button heights (numbers)
btn_height_100: number; // 55
btn_height_200: number; // 50
// ...
// Colors
primary_100: string; // Blue shades
primary_200: string;
primary_300: string;
success_100: string; // Green shades
success_200: string;
success_300: string;
error_100: string; // Red shades
error_200: string;
error_300: string;
// Text
text_100: string; // Main text color
secondary_100: string; // Secondary text
// Inputs
input_bg_100: string;
input_border_300: string;
input_height_300: number;
input_radius_300: number;
// Gradient
mainGradientColor: {
background: string; // CSS gradient
};
}import { Box, MainText } from "@core/ui";
import { observer } from 'mobx-react-lite';
import { themeStore } from '@modules/theme/stores';
export const MyComponent = observer(() => {
const { currentTheme } = themeStore;
return (
<Box
bRad={currentTheme.radius_300} // Here
bgColor={currentTheme.bg_100} // Here
>
// Text components from @core/ui already connected to currentTheme ;)
<MainText>
Hello World! MainText using currentTheme.text_100!
</MainText>
</Box>
);
});// Change entire theme
themeStore.changeTheme({
bg_000: "rgba(18, 18, 18, 1)",
bg_100: "rgba(24, 24, 24, 1)",
text_100: "rgba(255, 255, 255, 1)",
// ... dark theme values
});
// Change single value
themeStore.setThemeValue('primary_100', 'rgba(255, 0, 0, 1)');
// Set complete theme object
themeStore.setCurrentTheme(darkTheme);const darkTheme: ThemeT = {
bg_000: "rgba(0, 0, 0, 1)",
bg_100: "rgba(18, 18, 18, 1)",
bg_200: "rgba(28, 28, 28, 1)",
bg_300: "rgba(38, 38, 38, 1)",
// ...
text_100: "rgba(255, 255, 255, 1)",
secondary_100: "rgba(156, 156, 156, 1)",
border_100: "rgba(48, 48, 48, 1)",
// ...
};
// Apply it
themeStore.changeTheme(darkTheme);All core/ui components automatically use theme:
// ButtonUi uses theme colors
<ButtonUi
text="Click me"
onPress={handlePress}
// Uses theme.primary_100 by default
/>
// InputUi uses theme
<InputUi
placeholder="Enter text"
// Uses theme.input_bg_100, theme.input_border_300, etc.
/>import { logger } from '@lib/helpers';
logger.info('Component', 'User clicked button');
logger.success('API', 'Data loaded successfully');
logger.warning('Cache', 'Cache miss, fetching...');
logger.error('Network', 'Request failed');import { navigate } from '@lib/navigation';
class SomeClass {
constructor() { makeAutoObservable(this) };
someFunction = () => {
navigate("SignIn") // Use navigate in MobX
// Yes, you can use navigate function from .ts files
// Outside components. Everywhere!
}
}Create your first module:
modules/your-feature/
βββ pages/
β βββ YourPage/
β βββ YourPage.tsx
βββ stores/
β βββ your-actions/
β β βββ your-actions.ts # HTTP requests
β βββ your-interactions/
β β βββ your-interactions.ts # UI logic
β βββ your-service/
β β βββ your-service.ts # Business logic
β βββ index.ts # Re-exports
βββ widgets/
β βββ YourWidget/
βββ shared/
β βββ config/
β βββ schemas/
β βββ idk/
βββ hooks/
βββ components/
βββ etc.../
You can change whatever you want, lib, ui, or something else.
Telegram: @nics51
Questions? Issues? Feature requests? Hit me up! [Or create an issue]
Made with β€οΈ and lots of π§ from Kazakhstan π°πΏ for Pinely β¨



