Overview
Open Mushaf Native uses Jotai for atomic state management combined with MMKV for high-performance persistent storage. This architecture provides a simple, scalable approach to managing application state across native and web platforms.
Why Jotai?
Atomic Minimal re-renders with fine-grained reactivity
TypeScript Full type safety with zero configuration
Simple API React hooks-based, easy to learn
Async Native Built-in async atom support
Storage Architecture
The app uses a platform-specific storage layer that automatically switches between MMKV (native) and localStorage (web):
Native Implementation
Web Implementation
utils/storage/createStorage.ts
import { createJSONStorage } from 'jotai/utils' ;
import { MMKV } from 'react-native-mmkv' ;
const mmkv = new MMKV ();
const storage = {
getItem : ( key : string ) : string | null => mmkv . getString ( key ) ?? null ,
setItem : ( key : string , value : string ) : void => mmkv . set ( key , value ),
removeItem : ( key : string ) : void => mmkv . delete ( key ),
subscribe : (
key : string ,
callback : ( value : string | null ) => void ,
): (() => void ) => {
const listener = ( changedKey : string ) => {
if ( changedKey === key ) callback ( storage . getItem ( key ));
};
const { remove } = mmkv . addOnValueChangedListener ( listener );
return () => remove ();
},
};
export function createStorage < T >() {
return createJSONStorage < T >(() => storage );
}
MMKV provides synchronous, thread-safe storage that’s significantly faster than AsyncStorage.
utils/storage/createStorage.web.ts
import { createJSONStorage } from 'jotai/utils' ;
export function createStorage < T >() {
return createJSONStorage < T >();
}
The web version automatically uses localStorage via Jotai’s default implementation.
Creating Persistent Atoms
The app provides a helper function to create atoms with automatic persistence:
jotai/createAtomWithStorage.ts
import { atomWithStorage } from 'jotai/utils' ;
import { createStorage } from '@/utils/storage/createStorage' ;
export function createAtomWithStorage < T >( key : string , initialValue : T ) {
return atomWithStorage < T >( key , initialValue , createStorage < T >(), {
getOnInit: true ,
});
}
Key Features
Type-safe : Generic type parameter ensures type safety
Auto-hydration : getOnInit: true loads persisted value immediately
Platform-agnostic : Works on iOS, Android, and Web
Automatic serialization : JSON serialization handled by Jotai
State Organization
All global atoms are defined in jotai/atoms.ts and organized by feature:
UI State Atoms
Bottom Menu
Top Menu
Reading Banner
export const bottomMenuState = createAtomWithStorage < boolean >(
'BottomMenuState' ,
true ,
);
Reading State Atoms
Current Page
Mushaf Riwaya
Mushaf Contrast
export const currentSavedPage = createAtomWithStorage < number >(
'CurrentSavedPage' ,
1 ,
);
Feature State Atoms
Advanced Search
Tafseer Tab
Flip Sound
export const advancedSearch = createAtomWithStorage < boolean >(
'AdvancedSearch' ,
false ,
);
Settings & Notifications
Tutorial State
Hizb Notification
Daily Tracker Goal
export const finishedTutorial = createAtomWithStorage < boolean | undefined >(
'FinishedTutorial' ,
undefined ,
);
Advanced Patterns
1. Atoms with Date-based Reset
Some atoms automatically reset based on the current date:
import { observe } from 'jotai-effect' ;
type DailyTrackerProgress = {
value : number ;
date : string ;
};
export const dailyTrackerCompleted =
createAtomWithStorage < DailyTrackerProgress >( 'DailyTrackerCompleted' , {
value: 0 ,
date: new Date (). toDateString (),
});
// Auto-reset when date changes
observe (( get , set ) => {
( async () => {
const stored = await get ( dailyTrackerCompleted );
const today = new Date (). toDateString ();
if ( stored . date !== today ) {
set ( dailyTrackerCompleted , { value: 0 , date: today });
}
})();
});
The observe function from jotai-effect enables reactive side effects that run when atoms change.
The top menu automatically hides after a configured duration:
observe (( get , set ) => {
const duration = parseInt (
process . env . EXPO_PUBLIC_TOP_MENU_HIDE_DURATION_MS || '5000' ,
10 ,
);
if ( get ( topMenuState )) {
const timerId = setTimeout (() => {
set ( topMenuState , false );
}, duration );
return () => clearTimeout ( timerId );
}
});
3. Yesterday Page Tracking
Tracks the last page from the previous day for reading continuity:
type PageWithDate = {
value : number ;
date : string ;
};
export const yesterdayPage = createAtomWithStorage < PageWithDate >(
'YesterdayPage' ,
{
value: 1 ,
date: new Date (). toDateString (),
},
);
// Update yesterday page when date changes
observe (( get , set ) => {
( async () => {
const today = new Date (). toDateString ();
const saved = await get ( yesterdayPage );
const lastPage = await get ( currentSavedPage );
if ( saved . date !== today ) {
set ( yesterdayPage , { value: lastPage , date: today });
}
})();
});
Using Atoms in Components
Reading Atom Values
import { useAtomValue } from 'jotai/react' ;
import { bottomMenuState } from '@/jotai/atoms' ;
export function MyComponent () {
const menuVisible = useAtomValue ( bottomMenuState );
return (
< View style = {{ display : menuVisible ? 'flex' : 'none' }} >
{ /* ... */ }
</ View >
);
}
Writing Atom Values
import { useAtom } from 'jotai/react' ;
import { currentSavedPage } from '@/jotai/atoms' ;
export function PageNavigator () {
const [ page , setPage ] = useAtom ( currentSavedPage );
return (
< Button onPress = {() => setPage ( page + 1)} >
Next Page
</ Button >
);
}
Write-only Access
import { useSetAtom } from 'jotai/react' ;
import { topMenuState } from '@/jotai/atoms' ;
export function MenuToggle () {
const setMenuState = useSetAtom ( topMenuState );
return (
< Button onPress = {() => setMenuState ( true )} >
Show Menu
</ Button >
);
}
Atomic Updates Only components using changed atoms re-render
Synchronous Storage MMKV provides instant read/write operations
Lazy Initialization Atoms only initialize when first accessed
Subscription Based Storage changes trigger automatic updates
Debugging
Jotai DevTools integration (development only):
import { DevTools } from 'jotai-devtools' ;
// In your root component
{ __DEV__ && < DevTools />}
Best Practices
Use descriptive storage keys : Keys should be unique and descriptive
Define types explicitly : Always specify generic type parameter
Group related atoms : Organize atoms by feature or domain
Avoid atom bloat : Don’t create atoms for purely local component state
Use effects sparingly : Only for cross-cutting concerns like auto-reset logic
For complex derived state, consider using Jotai’s atom((get) => ...) pattern instead of creating additional persistent atoms.