Akar Masalah Flash of Unauthenticated Screen

Saat aplikasi React Native melakukan cold start, sistem operasi menjalankan proses native, menginisialisasi JavaScript Virtual Machine (Hermes), lalu mengeksekusi bundel JavaScript. Masalah flash of unauthenticated screen (tampilan login muncul sekilas sebelum masuk ke dashboard) berakar dari ketergantungan arsitektur pada asynchronous storage seperti @react-native-async-storage/async-storage.

AsyncStorage berkomunikasi melalui asynchronous bridge atau asynchronous JSI call. Operasi pembacaan token autentikasi (await AsyncStorage.getItem('token')) memerlukan minimal satu tick pada event loop. Akibatnya, React merender frame pertama dengan state default (misalnya token = null atau isAuthenticated = false). Navigator langsung mengarahkan rute ke AuthStack. Begitu promise penyimpanan selesai, state diperbarui menjadi terautentikasi dan navigator melompat ke AppStack. Transisi mendadak inilah yang menyebabkan flicker UI.

Akses Sinkron Menggunakan react-native-mmkv

Solusi arsitektural untuk mengeliminasi siklus render perantara adalah membaca data persistensi secara sinkron sebelum render pertama dijalankan. Library react-native-mmkv mengimplementasikan library key-value berbasis C++ milik Tencent yang memanfaatkan mmap (memory-mapped files) dan terekspos ke runtime JavaScript melalui JSI (JavaScript Interface).

Karena pembacaan bersifat sinkron, nilai autentikasi dapat langsung dibaca saat inisialisasi state React tanpa penundaan promise:

  • Tanpa Async Bridge: Eksekusi pemanggilan nilai langsung mengakses pointer memori native C++.
  • Zero-delay Initial State: useState dapat langsung diisi dengan nilai tersimpan saat fase pembuatan komponen, bukan di dalam useEffect.

Implementasi Sinkron Storage

Inisialisasi instance storage MMKV terpusat. Instance ini dapat diproteksi menggunakan enkripsi native jika menyimpan data sensitif.

// src/services/storage.ts
import { MMKV } from 'react-native-mmkv';

export const storage = new MMKV({
  id: 'app-user-storage',
  encryptionKey: 'secure-encryption-key-from-keystore', // opsional: gunakan key dari react-native-keychain
});

export const StorageKeys = {
  AUTH_TOKEN: 'auth_token',
  USER_DATA: 'user_data',
} as const;

Orkestrasi Cold Boot dengan react-native-bootsplash

Kendati pembacaan storage berlangsung sinkron, inisialisasi pohon navigasi tetap membutuhkan waktu beberapa frame. Jika splash screen native hilang sebelum navigasi menyelesaikan layout pertama, layar kosong atau parsial tetap berisiko terlihat.

Pustaka react-native-bootsplash menahan native splash screen tetap berada di atas layar sampai aplikasi siap seutuhnya, lalu menyembunyikannya dengan transisi fade.

Konfigurasi Root Auth Provider

// src/context/AuthContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { storage, StorageKeys } from '../services/storage';

interface AuthContextType {
  token: string | null;
  signIn: (newToken: string) => void;
  signOut: () => void;
}

const AuthContext = createContext<AuthContextType>({} as AuthContextType);

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  // Evaluasi sinkron: langsung bernilai string atau null pada first render
  const [token, setToken] = useState<string | null>(() => {
    return storage.getString(StorageKeys.AUTH_TOKEN) ?? null;
  });

  const signIn = (newToken: string) => {
    storage.set(StorageKeys.AUTH_TOKEN, newToken);
    setToken(newToken);
  };

  const signOut = () => {
    storage.delete(StorageKeys.AUTH_TOKEN);
    setToken(null);
  };

  return (
    <AuthContext.Provider value={{ token, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

Navigasi dan Penutupan Splash Screen Presisi

Pasang listener onReady pada NavigationContainer. Sembunyikan bootsplash secara terprogram hanya ketika hierarchy navigasi selesai dipasang ke layer native.

// src/App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import BootSplash from 'react-native-bootsplash';

import { AuthProvider, useAuth } from './context/AuthContext';
import { LoginScreen } from './screens/LoginScreen';
import { DashboardScreen } from './screens/DashboardScreen';

const Stack = createNativeStackNavigator();

const RootNavigator = () => {
  const { token } = useAuth();

  return (
    <NavigationContainer
      onReady={() => {
        // Tahan splash hingga struktur navigator sepenuhnya siap di-render
        BootSplash.hide({ fade: true });
      }}
    >
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        {token ? (
          <Stack.Screen name="Dashboard" component={DashboardScreen} />
        ) : (
          <Stack.Screen name="Login" component={LoginScreen} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default function App() {
  return (
    <AuthProvider>
      <RootNavigator />
    </AuthProvider>
  );
}

Analisis Perbandingan: AsyncStorage vs MMKV

KriteriaAsyncStoragereact-native-mmkv
Metode AksesAsynchronous (Promise-based)Synchronous (JSI)
State HydrationMembutuhkan status isLoadingInstant pada inisialisasi state
Flicker RiskTinggi (jika rute auth dinamis)Nol (terbaca sebelum first paint)
Overhead I/OTinggi (serialisasi JSON & Bridge)Sangat rendah (Memory-mapped file)

Trade-off dan Keamanan

Meskipun MMKV memecahkan masalah latensi dan flicker, pertimbangkan batasan berikut:

  • Enkripsi: MMKV mendukung enkripsi bawaan via AES-CFB, tetapi kunci enkripsinya harus disimpan aman di Android Keystore atau iOS Keychain (misalnya via react-native-keychain). Jangan simpan string kunci mentah (hardcoded) di JavaScript.
  • Ukuran Data Besar: MMKV ditujukan untuk data key-value berukuran kecil hingga menengah (token, preferensi pengguna, cached session metadata). Hindari menyimpan payload masif ratusan megabyte yang membebani memori virtual.
  • Validasi Token Network: Jika token harus diverifikasi ke server saat boot, biarkan splash screen menahan tampilan sembari memproses request via Promise.all sebelum memanggil BootSplash.hide().