Skip to main content

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):
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.

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

export const bottomMenuState = createAtomWithStorage<boolean>(
  'BottomMenuState',
  true,
);

Reading State Atoms

export const currentSavedPage = createAtomWithStorage<number>(
  'CurrentSavedPage',
  1,
);

Feature State Atoms

export const advancedSearch = createAtomWithStorage<boolean>(
  'AdvancedSearch',
  false,
);

Settings & Notifications

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:
jotai/atoms.ts
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.

2. Auto-hide Menu Effect

The top menu automatically hides after a configured duration:
jotai/atoms.ts
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:
jotai/atoms.ts
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

Example Component
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

Example with Updates
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

Example with Setter
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>
  );
}

Performance Considerations

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

  1. Use descriptive storage keys: Keys should be unique and descriptive
  2. Define types explicitly: Always specify generic type parameter
  3. Group related atoms: Organize atoms by feature or domain
  4. Avoid atom bloat: Don’t create atoms for purely local component state
  5. 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.

Build docs developers (and LLMs) love