@ridwan-retainer/app-shell
v0.1.1
Published
Foundational runtime shell for app lifecycle, providers, and navigation
Downloads
188
Maintainers
Readme
@ridwan-retainer/app-shell
A comprehensive, production-ready application runtime framework for React Native apps. Provides lifecycle management, state management, navigation, error handling, remote configuration, and more out of the box.
📋 Table of Contents
- Features
- Installation
- Quick Start
- Architecture
- API Reference
- Complete Examples
- Best Practices
- Migration Guide
- Troubleshooting
✨ Features
Core Features
- 🔄 Lifecycle Management: Comprehensive app lifecycle hooks and state management
- 🏪 State Management: Built-in Zustand stores for theme, settings, feature flags, and telemetry
- 🧭 Navigation: Type-safe React Navigation integration with deep linking
- ❌ Error Handling: Global error boundaries with Sentry integration
- ⏳ Loading States: Sophisticated loading and splash screen management
- 🔧 Remote Config: Firebase Remote Config integration for feature flags
- 🔌 Adapter System: Plugin architecture for extending functionality
- 📦 Provider Composition: Clean provider nesting and composition
- 📝 TypeScript: Full type safety throughout
- ⚡ Performance: Optimized rendering and state updates
- ✅ Well-tested: Extensive test coverage
State Stores
- Theme: Dark/light mode with custom theme support
- Settings: App-wide settings persistence
- Feature Flags: Runtime feature toggles
- Telemetry: Usage tracking and analytics
📦 Installation
npm install @ridwan-retainer/app-shellRequired Peer Dependencies
npm install @react-navigation/native \
react-native-screens \
react-native-safe-area-context \
react \
react-nativeOptional Peer Dependencies
For full functionality:
# For AsyncStorage persistence
npm install @react-native-async-storage/async-storage
# For Remote Config
npm install @react-native-firebase/app @react-native-firebase/remote-config
# For Error Tracking
npm install @sentry/react-native
# For Splash Screens
npm install react-native-bootsplash🚀 Quick Start
Basic Setup
import { LifecycleProvider } from '@ridwan-retainer/app-shell';
import { NavigationContainer } from '@react-navigation/native';
function App() {
return (
<LifecycleProvider
onInitialize={async () => {
// Initialize your app (API clients, analytics, etc.)
console.log('App initializing...');
}}
onReady={() => {
// App is ready to use
console.log('App ready!');
}}
>
<NavigationContainer>
<YourAppContent />
</NavigationContainer>
</LifecycleProvider>
);
}With Error Boundary
import {
LifecycleProvider,
ErrorBoundary,
ErrorFallback,
} from '@ridwan-retainer/app-shell';
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, stackTrace) => {
console.error('App error:', error, stackTrace);
// Report to error tracking service
}}
>
<LifecycleProvider>
<YourApp />
</LifecycleProvider>
</ErrorBoundary>
);
}With All Features
import {
LifecycleProvider,
ErrorBoundary,
LoadingProvider,
RemoteConfigProvider,
initializeSentry,
} from '@ridwan-retainer/app-shell';
// Initialize Sentry
initializeSentry({
dsn: 'your-sentry-dsn',
environment: __DEV__ ? 'development' : 'production',
});
function App() {
return (
<ErrorBoundary>
<LifecycleProvider
onInitialize={async () => {
// Load critical data
await loadAuthState();
await initializeAnalytics();
}}
>
<RemoteConfigProvider>
<LoadingProvider>
<NavigationContainer>
<YourApp />
</NavigationContainer>
</LoadingProvider>
</RemoteConfigProvider>
</LifecycleProvider>
</ErrorBoundary>
);
}🏗 Architecture
Provider Hierarchy
ErrorBoundary (Top-level error catching)
└─ LifecycleProvider (App lifecycle management)
└─ RemoteConfigProvider (Feature flags & config)
└─ LoadingProvider (Loading states & splash)
└─ NavigationContainer (React Navigation)
└─ Your App ContentData Flow
User Action → State Update → Zustand Store → Component Re-render
↓
Side Effects (Persistence, Analytics, etc.)📚 API Reference
Lifecycle Management
<LifecycleProvider />
Manages app lifecycle and initialization.
import { LifecycleProvider } from '@ridwan-retainer/app-shell';
<LifecycleProvider
onInitialize={async () => {
// Run once on app start
await loadUserData();
await connectWebSocket();
}}
onReady={() => {
// Called when initialization completes
console.log('App is ready!');
}}
onBackground={() => {
// App moved to background
saveCurrentState();
}}
onForeground={() => {
// App returned to foreground
refreshData();
}}
>
{children}
</LifecycleProvider>Props:
{
onInitialize?: () => Promise<void> | void;
onReady?: () => void;
onBackground?: () => void;
onForeground?: () => void;
onError?: (error: Error) => void;
children: ReactNode;
}useAppState()
Monitor app foreground/background state.
import { useAppState } from '@ridwan-retainer/app-shell';
function DataSync() {
const appState = useAppState();
useEffect(() => {
if (appState === 'active') {
syncData();
}
}, [appState]);
return null;
}Returns: 'active' | 'background' | 'inactive'
useStartupType()
Determine if app is cold start or warm start.
import { useStartupType } from '@ridwan-retainer/app-shell';
function Analytics() {
const startupType = useStartupType();
useEffect(() => {
logEvent('app_start', { type: startupType });
}, []);
}Returns: 'cold' | 'warm'
LifecycleManager
Programmatic lifecycle control.
import { LifecycleManager } from '@ridwan-retainer/app-shell';
// Get lifecycle instance
const lifecycle = LifecycleManager.getInstance();
// Listen to events
lifecycle.on('initialize', async () => {
await setupApp();
});
lifecycle.on('ready', () => {
console.log('App ready');
});
// Trigger lifecycle phase
await lifecycle.initialize();State Management
All stores are built with Zustand and include:
- Type-safe actions and selectors
- Automatic persistence (where applicable)
- Optimized re-renders
Theme Store
import { useTheme, useThemeActions } from '@ridwan-retainer/app-shell';
function ThemeToggle() {
const theme = useTheme();
const { toggleTheme, setTheme } = useThemeActions();
return (
<View>
<Text>Current theme: {theme.mode}</Text>
<Button title="Toggle Theme" onPress={toggleTheme} />
<Button title="Dark Mode" onPress={() => setTheme('dark')} />
<Button title="Light Mode" onPress={() => setTheme('light')} />
</View>
);
}Theme Type:
{
mode: 'light' | 'dark';
colors: {
primary: string;
background: string;
text: string;
border: string;
// ... more colors
};
}Custom Theme:
import { useThemeActions } from '@ridwan-retainer/app-shell';
const customTheme = {
mode: 'dark',
colors: {
primary: '#FF6B6B',
background: '#1A1A1A',
text: '#FFFFFF',
// ...
},
};
const { setCustomTheme } = useThemeActions();
setCustomTheme(customTheme);Settings Store
import {
useSettings,
useSettingsActions,
} from '@ridwan-retainer/app-shell';
function SettingsScreen() {
const settings = useSettings();
const { updateSetting, resetSettings } = useSettingsActions();
return (
<View>
<Switch
value={settings.notifications}
onValueChange={(value) =>
updateSetting('notifications', value)
}
/>
<Switch
value={settings.analytics}
onValueChange={(value) =>
updateSetting('analytics', value)
}
/>
<Button title="Reset" onPress={resetSettings} />
</View>
);
}Built-in Settings:
{
notifications: boolean;
analytics: boolean;
[key: string]: any; // Custom settings
}Feature Flags Store
import {
useFeatureFlag,
useFeatureFlagActions,
} from '@ridwan-retainer/app-shell';
function NewFeature() {
const isEnabled = useFeatureFlag('new_feature');
const { setFlag } = useFeatureFlagActions();
// Enable for testing
useEffect(() => {
if (__DEV__) {
setFlag('new_feature', true);
}
}, []);
if (!isEnabled) return null;
return <NewFeatureComponent />;
}Actions:
{
setFlag: (key: string, value: boolean) => void;
setFlags: (flags: Record<string, boolean>) => void;
resetFlags: () => void;
}Telemetry Store
import {
useTelemetry,
useTelemetryActions,
} from '@ridwan-retainer/app-shell';
function ScreenView({ screenName }: { screenName: string }) {
const { trackEvent } = useTelemetryActions();
useEffect(() => {
trackEvent('screen_view', { screen: screenName });
}, [screenName]);
return null;
}
// Usage
function ProfileScreen() {
return (
<>
<ScreenView screenName="profile" />
<ProfileContent />
</>
);
}Telemetry Actions:
{
trackEvent: (name: string, properties?: Record<string, any>) => void;
trackScreen: (screenName: string) => void;
setUserId: (userId: string) => void;
clearUserId: () => void;
}Navigation
<NavigationContainer />
Type-safe navigation container wrapper.
import { NavigationContainer } from '@ridwan-retainer/app-shell';
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
};
function App() {
return (
<NavigationContainer<RootStackParamList>
linking={{
prefixes: ['myapp://'],
config: {
screens: {
Home: 'home',
Profile: 'profile/:userId',
Settings: 'settings',
},
},
}}
onReady={() => {
console.log('Navigation ready');
}}
>
<RootNavigator />
</NavigationContainer>
);
}useTypedNavigation()
Type-safe navigation hook.
import { useTypedNavigation } from '@ridwan-retainer/app-shell';
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
};
function HomeScreen() {
const navigation = useTypedNavigation<RootStackParamList>();
const goToProfile = (userId: string) => {
// Type-safe: TypeScript knows userId is required
navigation.navigate('Profile', { userId });
};
return <Button title="View Profile" onPress={() => goToProfile('123')} />;
}useTypedRoute()
Type-safe route params hook.
import { useTypedRoute } from '@ridwan-retainer/app-shell';
type ProfileScreenParams = {
userId: string;
tab?: 'posts' | 'photos';
};
function ProfileScreen() {
const route = useTypedRoute<ProfileScreenParams>();
const { userId, tab = 'posts' } = route.params;
return (
<View>
<Text>User ID: {userId}</Text>
<Text>Active Tab: {tab}</Text>
</View>
);
}navigationRef
Programmatic navigation without hooks.
import { navigationRef } from '@ridwan-retainer/app-shell';
// Navigate from anywhere (e.g., push notification handler)
function handleNotificationPress(notificationData: any) {
if (navigationRef.isReady()) {
navigationRef.navigate('Notification', {
id: notificationData.id,
});
}
}Error Handling
<ErrorBoundary />
Catch and handle React errors.
import { ErrorBoundary, ErrorFallback } from '@ridwan-retainer/app-shell';
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
// Log to error tracking service
Sentry.captureException(error, { extra: errorInfo });
}}
onReset={() => {
// Reset app state if needed
resetAppState();
}}
>
<App />
</ErrorBoundary>Props:
{
FallbackComponent: React.ComponentType<FallbackProps>;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
onReset?: () => void;
resetKeys?: string[]; // Keys that trigger reset when changed
children: ReactNode;
}<ErrorFallback />
Default error UI component.
import { ErrorFallback } from '@ridwan-retainer/app-shell';
// Use as-is
<ErrorBoundary FallbackComponent={ErrorFallback}>
// Or customize
function CustomErrorFallback({ error, resetError }: FallbackProps) {
return (
<View>
<Text>Something went wrong</Text>
<Text>{error.message}</Text>
<Button title="Try Again" onPress={resetError} />
</View>
);
}useErrorHandler()
Programmatically handle errors.
import { useErrorHandler } from '@ridwan-retainer/app-shell';
function DataLoader() {
const handleError = useErrorHandler();
const loadData = async () => {
try {
const data = await fetchData();
return data;
} catch (error) {
// This will trigger the nearest ErrorBoundary
handleError(error);
}
};
return <Button onPress={loadData} />;
}initializeSentry()
Initialize Sentry error tracking.
import { initializeSentry } from '@ridwan-retainer/app-shell';
initializeSentry({
dsn: 'your-sentry-dsn',
environment: __DEV__ ? 'development' : 'production',
enableInExpoDevelopment: false,
debug: __DEV__,
tracesSampleRate: 1.0,
});Loading & Splash
<LoadingProvider />
Manage loading states and splash screen.
import { LoadingProvider } from '@ridwan-retainer/app-shell';
<LoadingProvider
onReady={() => {
console.log('App ready, hiding splash');
}}
>
{children}
</LoadingProvider>useLoadingState()
Control loading states.
import { useLoadingState } from '@ridwan-retainer/app-shell';
function DataFetcher() {
const { setLoading, clearLoading } = useLoadingState();
const fetchData = async () => {
setLoading('data_fetch', 'Loading data...');
try {
const data = await api.getData();
return data;
} finally {
clearLoading('data_fetch');
}
};
return <Button onPress={fetchData} />;
}useAppReady()
Check if app is ready.
import { useAppReady } from '@ridwan-retainer/app-shell';
function App() {
const isReady = useAppReady();
if (!isReady) {
return <SplashScreen />;
}
return <MainApp />;
}useSplashControl()
Manually control splash screen.
import { useSplashControl } from '@ridwan-retainer/app-shell';
function Initializer() {
const { hideSplash } = useSplashControl();
useEffect(() => {
const init = async () => {
await loadCriticalData();
await setupServices();
hideSplash(); // Hide splash when ready
};
init();
}, []);
return null;
}Remote Config
<RemoteConfigProvider />
Integrate Firebase Remote Config.
import { RemoteConfigProvider } from '@ridwan-retainer/app-shell';
<RemoteConfigProvider
defaults={{
enable_new_feature: false,
api_timeout: 5000,
theme_color: '#007AFF',
}}
minimumFetchInterval={3600} // 1 hour
>
{children}
</RemoteConfigProvider>useFeatureFlag()
Check feature flags from Remote Config.
import { useFeatureFlag } from '@ridwan-retainer/app-shell';
function ExperimentalFeature() {
const isEnabled = useFeatureFlag('enable_new_ui');
if (!isEnabled) return null;
return <NewUIComponent />;
}useConfigValue()
Get any config value.
import { useConfigValue } from '@ridwan-retainer/app-shell';
function ApiClient() {
const timeout = useConfigValue<number>('api_timeout', 5000);
const baseUrl = useConfigValue<string>('api_base_url', 'https://api.example.com');
// Use in fetch config
return { timeout, baseUrl };
}useRemoteConfig()
Access full Remote Config API.
import { useRemoteConfig } from '@ridwan-retainer/app-shell';
function ConfigDebug() {
const { config, isLoading, refresh } = useRemoteConfig();
return (
<View>
<Text>Config: {JSON.stringify(config, null, 2)}</Text>
<Button title="Refresh" onPress={refresh} disabled={isLoading} />
</View>
);
}Adapters
The adapter system allows you to extend app-shell functionality.
Creating an Adapter
import { AdapterDefinition } from '@ridwan-retainer/app-shell';
interface AnalyticsAdapter {
trackEvent: (event: string, properties?: any) => void;
trackScreen: (screenName: string) => void;
setUserId: (userId: string) => void;
}
const analyticsAdapter: AdapterDefinition<AnalyticsAdapter> = {
name: 'analytics',
version: '1.0.0',
initialize: async (config) => {
// Initialize analytics SDK
await AnalyticsSDK.init(config.apiKey);
return {
trackEvent: (event, properties) => {
AnalyticsSDK.track(event, properties);
},
trackScreen: (screenName) => {
AnalyticsSDK.screen(screenName);
},
setUserId: (userId) => {
AnalyticsSDK.identify(userId);
},
};
},
cleanup: async () => {
// Cleanup if needed
await AnalyticsSDK.flush();
},
};Using an Adapter
import { AdapterRegistry, useAdapter } from '@ridwan-retainer/app-shell';
// Register adapter
AdapterRegistry.register(analyticsAdapter);
// Use in component
function TrackingComponent() {
const analytics = useAdapter<AnalyticsAdapter>('analytics');
useEffect(() => {
analytics?.trackScreen('home');
}, []);
const trackClick = () => {
analytics?.trackEvent('button_click', { button: 'subscribe' });
};
return <Button onPress={trackClick} />;
}💡 Complete Examples
Example 1: Production App Setup
import {
ErrorBoundary,
LifecycleProvider,
LoadingProvider,
RemoteConfigProvider,
NavigationContainer,
initializeSentry,
} from '@ridwan-retainer/app-shell';
// Initialize error tracking
initializeSentry({ dsn: SENTRY_DSN });
function App() {
return (
<ErrorBoundary>
<LifecycleProvider
onInitialize={async () => {
// Initialize critical services
await initAuth();
await loadUserPreferences();
await connectAnalytics();
}}
onReady={() => {
console.log('App initialization complete');
}}
onBackground={() => {
// Save state when backgrounded
saveAppState();
}}
onForeground={() => {
// Refresh data when foregrounded
refreshData();
}}
>
<RemoteConfigProvider
defaults={{
enable_new_ui: false,
api_timeout: 5000,
}}
>
<LoadingProvider>
<NavigationContainer
linking={{
prefixes: ['myapp://'],
config: { screens: { /* ... */ } },
}}
>
<MainNavigator />
</NavigationContainer>
</LoadingProvider>
</RemoteConfigProvider>
</LifecycleProvider>
</ErrorBoundary>
);
}Example 2: Theme Switcher
import {
useTheme,
useThemeActions,
} from '@ridwan-retainer/app-shell';
function ThemeSwitcher() {
const theme = useTheme();
const { toggleTheme, setCustomTheme } = useThemeActions();
const applyCustomTheme = () => {
setCustomTheme({
mode: 'dark',
colors: {
primary: '#FF6B6B',
background: '#1A1A1A',
text: '#FFFFFF',
border: '#333333',
error: '#FF4444',
success: '#44FF44',
},
});
};
return (
<View style={{ backgroundColor: theme.colors.background }}>
<Text style={{ color: theme.colors.text }}>
Current Mode: {theme.mode}
</Text>
<Button title="Toggle Theme" onPress={toggleTheme} />
<Button title="Custom Theme" onPress={applyCustomTheme} />
</View>
);
}Example 3: Feature Flag System
import {
useFeatureFlag,
useFeatureFlagActions,
} from '@ridwan-retainer/app-shell';
// Admin panel to toggle features
function FeatureFlagsPanel() {
const { setFlag } = useFeatureFlagActions();
return (
<View>
<FlagToggle
name="new_ui"
label="New UI Design"
onToggle={(value) => setFlag('new_ui', value)}
/>
<FlagToggle
name="beta_features"
label="Beta Features"
onToggle={(value) => setFlag('beta_features', value)}
/>
</View>
);
}
// Feature component that respects flag
function NewUIFeature() {
const isEnabled = useFeatureFlag('new_ui');
if (!isEnabled) {
return <LegacyUI />;
}
return <NewUI />;
}Example 4: Navigation with Deep Linking
import {
NavigationContainer,
useTypedNavigation,
useTypedRoute,
} from '@ridwan-retainer/app-shell';
type RootParamList = {
Home: undefined;
Article: { id: string; source?: string };
Profile: { userId: string };
};
function App() {
return (
<NavigationContainer<RootParamList>
linking={{
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Home: '',
Article: 'article/:id',
Profile: 'user/:userId',
},
},
}}
>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Article" component={ArticleScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
function HomeScreen() {
const navigation = useTypedNavigation<RootParamList>();
const openArticle = (id: string) => {
navigation.navigate('Article', { id, source: 'home_feed' });
};
return <ArticleList onArticlePress={openArticle} />;
}
function ArticleScreen() {
const route = useTypedRoute<{ id: string; source?: string }>();
const { id, source } = route.params;
// Track where user came from
useEffect(() => {
trackEvent('article_view', { id, source });
}, [id, source]);
return <ArticleContent id={id} />;
}✅ Best Practices
1. Initialize Early
Place lifecycle initialization as high as possible in component tree:
// App.tsx - root file
<LifecycleProvider onInitialize={initializeApp}>
<RestOfApp />
</LifecycleProvider>2. Use Error Boundaries
Wrap different app sections with error boundaries:
<ErrorBoundary> {/* Top-level */}
<Navigation>
<ErrorBoundary> {/* Per-screen */}
<ComplexScreen />
</ErrorBoundary>
</Navigation>
</ErrorBoundary>3. Centralize State
Use provided stores for app-wide state:
// Don't create separate state management for theme, settings, etc.
// Use built-in stores
const theme = useTheme();
const settings = useSettings();4. Type Your Navigation
Always type your navigation stack:
type RootStackParamList = {
Home: undefined;
Detail: { id: string };
};
const navigation = useTypedNavigation<RootStackParamList>();5. Handle Loading States
Use loading provider for async operations:
const { setLoading, clearLoading } = useLoadingState();
const loadData = async () => {
setLoading('data', 'Loading...');
try {
await fetchData();
} finally {
clearLoading('data');
}
};6. Remote Config Defaults
Always provide defaults for remote config:
<RemoteConfigProvider
defaults={{
feature_enabled: false, // Safe default
timeout: 5000,
}}
>🐛 Troubleshooting
Issue: "Provider not found"
Solution: Ensure providers are in correct order:
<ErrorBoundary>
<LifecycleProvider>
<LoadingProvider>
{/* Your app */}
</LoadingProvider>
</LifecycleProvider>
</ErrorBoundary>Issue: Navigation types not working
Solution: Define param list type:
type RootParamList = {
Home: undefined;
// ...
};
const navigation = useTypedNavigation<RootParamList>();Issue: Remote Config not updating
Solution: Check fetch interval and force refresh:
const { refresh } = useRemoteConfig();
await refresh();Issue: Theme changes not persisting
Solution: Ensure AsyncStorage is linked:
npm install @react-native-async-storage/async-storage
cd ios && pod install📖 Migration Guide
From 0.0.x to 0.1.0
No breaking changes. Initial release.
🤝 Contributing
Contributions welcome! Submit PRs to GitHub repository.
📄 License
MIT © Ridwan Hamid
