NativeCraft is a cutting-edge React Native mobile application built with TypeScript and Expo, proudly featuring React Native's New Architecture for superior performance and future-proofing. It offers robust authentication, RTL/LTR language support, dark/light theme capabilities, and a modern UI experience. The app follows a structured, modular architecture with a focus on reusability, scalability, and maintainability.
src/ ├── assets/ # Images, icons, fonts, and other static assets ├── components/ # Reusable UI components ├── config/ # App configuration files ├── context/ # React Context providers (ThemeContext, etc.) ├── hooks/ # Custom React hooks ├── lang/ # i18n translation files ├── models/ # Data models and interfaces ├── navigation/ # Navigation configuration ├── redux/ # State management ├── screens/ # Screen components ├── styles/ # Global styles and themes ├── typings/ # Global TypeScript types └── utils/ # Utility functions
Clone the repository
git clone https://github.com/gulsher7/expo_boilerplate.git cd expo_boilerplate
Install dependencies
npm install # or yarn install
Firebase Configuration (Required)
google-services.json for Android and place it in the project rootGoogleService-Info.plist for iOS and place it in the project rootGenerate native code (Required for Firebase)
npx expo prebuild
Important: Since this app uses React Native Firebase, you must run expo prebuild to generate the native Android and iOS folders. This is required before running the app on devices or simulators.
Start the development server
npm start # or yarn start
Run on specific platform
# iOS npm run ios # Android npm run android
google-services.json and GoogleService-Info.plist) are already configured in app.jsonexpo prebuild command will properly integrate them into the native projectsNativeCraft proudly leverages React Native's New Architecture for enhanced performance and future-ready development:
💡 Pro Tip: The New Architecture is the future of React Native development, and NativeCraft is built from the ground up to take advantage of these improvements!
The app uses a Context-based theming system that allows for seamless switching between light and dark modes:
// Usage in components
const { theme, toggleTheme } = useTheme();
const colors = Colors[theme];
// Component example
const MyComponent = () => {
const { theme } = useTheme();
const colors = Colors[theme];
return (
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.text }}>Hello World</Text>
</View>
);
};Fonts are loaded at application startup and made available through a centralized font family object:
// Font loading in App.tsx
const [loaded, error] = useFonts({
"Inter-Regular": require("./src/assets/fonts/Inter-Regular.ttf"),
"Inter-Bold": require("./src/assets/fonts/Inter-Bold.ttf"),
"Inter-Medium": require("./src/assets/fonts/Inter-Medium.ttf"),
"Inter-SemiBold": require("./src/assets/fonts/Inter-SemiBold.ttf"),
});
// Usage with fontFamily utility
import fontFamily from '@/styles/fontFamily';
const styles = StyleSheet.create({
title: {
fontFamily: fontFamily.bold,
fontSize: 18,
}
});The app supports multiple languages with full RTL layout support using i18next:
// With the TextComp wrapper component
<TextComp text="LOGIN" />
// With variable substitution
<TextComp text="HELLO_USER" values={{ name: user.name }} />
// RTL Support
import { useTranslation } from 'react-i18next';
export default function useIsRTL() {
const { i18n } = useTranslation();
return i18n.language === 'ar';
}
// Usage in styles
const styles = StyleSheet.create({
container: {
flexDirection: isRTL ? 'row-reverse' : 'row',
textAlign: isRTL ? 'right' : 'left',
}
});The app uses a set of standardized components to ensure consistency:
<TextComp text="WELCOME" /> <TextComp isDynamic text="Hello World" />
<ButtonComp
text="LOGIN"
onPress={handleLogin}
variant="primary"
isLoading={isLoading}
/>The app uses Redux Toolkit for centralized state management:
// Using Redux in Components
import { useDispatch, useSelector } from 'react-redux';
import actions from '@/redux/actions';
import { RootState } from '@/redux/store';
const Component = () => {
const dispatch = useDispatch();
const { user } = useSelector((state: RootState) => state.auth);
const handleLogin = async () => {
await dispatch(actions.login(credentials));
};
return (/* Component JSX */);
};The app uses a centralized API service based on Axios with automatic token handling and error management:
// API Actions with Redux Toolkit
export const login = createAsyncThunk(
'auth/login',
async (credentials, { rejectWithValue }) => {
try {
const response = await apiInstance.post('/auth/login', credentials);
return response;
} catch (error) {
return rejectWithValue(error.response?.data || 'Login failed');
}
}
);The app uses Expo's SecureStore for encrypted storage with helper methods for objects:
// Storage usage
import { secureStorage } from '@/utils/secureStorage';
// Store data
await secureStorage.setItem('AUTH_TOKEN', token);
await secureStorage.setObject('USER_DATA', userData);
// Retrieve data
const token = await secureStorage.getItem('AUTH_TOKEN');
const userData = await secureStorage.getObject('USER_DATA');The app uses React Native Firebase for push notifications and other Firebase services:
// Firebase messaging setup
import messaging from '@react-native-firebase/messaging';
// Get FCM token
const getFCMToken = async () => {
try {
const token = await messaging().getToken();
console.log('FCM Token:', token);
return token;
} catch (error) {
console.log('Error getting FCM token:', error);
}
};
// Handle foreground messages
messaging().onMessage(async remoteMessage => {
console.log('A new FCM message arrived!', JSON.stringify(remoteMessage));
});The app uses a scaling utility for responsive dimensions:
// Using Scale in Styles
import { moderateScale } from '@/styles/scaling';
const styles = StyleSheet.create({
container: {
padding: moderateScale(16),
marginBottom: moderateScale(20),
},
title: {
fontSize: moderateScale(18),
},
});google-services.json and GoogleService-Info.plist are in the project rootnpx expo prebuild --clean to regenerate native foldersapp.jsonnpx expo start --clearnpx expo prebuild --cleancd ios && pod installandroid/ and ios/ foldersnpx expo prebuild --cleanThe app uses a Context-based theming system that allows for seamless switching between light and dark modes:
// src/context/ThemeContext.tsx
import React, { createContext, useState, useEffect, useContext } from 'react';
import { secureStorage } from '@/utils/secureStorage';
import { Colors, ThemeType } from '@/styles/colors';
// Usage in components
const { theme, toggleTheme } = useTheme();
const colors = Colors[theme];Key Features:
Using Theme Colors:
// Component example
import { useTheme } from '@/context/ThemeContext';
import { Colors } from '@/styles/colors';
const MyComponent = () => {
const { theme } = useTheme();
const colors = Colors[theme];
return (
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.text }}>Hello World</Text>
</View>
);
};Fonts are loaded at application startup and made available through a centralized font family object:
// Font loading in App.tsx
const [loaded, error] = useFonts({
"Inter-Regular": require("./src/assets/fonts/Inter-Regular.ttf"),
"Inter-Bold": require("./src/assets/fonts/Inter-Bold.ttf"),
"Inter-Medium": require("./src/assets/fonts/Inter-Medium.ttf"),
"Inter-SemiBold": require("./src/assets/fonts/Inter-SemiBold.ttf"),
});
// Usage with fontFamily utility
import fontFamily from '@/styles/fontFamily';
const styles = StyleSheet.create({
title: {
fontFamily: fontFamily.bold,
fontSize: 18,
}
});Font Family Structure:
// src/styles/fontFamily.ts
export default {
regular: 'Inter-Regular',
medium: 'Inter-Medium',
semiBold: 'Inter-SemiBold',
bold: 'Inter-Bold',
};The app supports multiple languages with full RTL layout support using i18next:
// src/lang/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import 'intl-pluralrules';
import en from './en.json';
import ar from './ar.json';
i18n
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
ar: { translation: ar }
},
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false
}
});
export default i18n;Translation Files Structure:
// en.json
{
"LOGIN": "Login",
"WELCOME_MESSAGE": "Welcome to NativeCraft"
}
// ar.json
{
"LOGIN": "تسجيل الدخول",
"WELCOME_MESSAGE": "مرحبًا بك في NativeCraft"
}Using Translations:
// With the TextComp wrapper component
<TextComp text="LOGIN" />
// With variable substitution
<TextComp text="HELLO_USER" values={{ name: user.name }} />RTL Support:
// Custom hook for RTL detection
import { useTranslation } from 'react-i18next';
export default function useIsRTL() {
const { i18n } = useTranslation();
return i18n.language === 'ar';
}
// Usage in styles
const styles = StyleSheet.create({
container: {
flexDirection: isRTL ? 'row-reverse' : 'row',
textAlign: isRTL ? 'right' : 'left',
}
});The app uses a set of standardized components to ensure consistency:
TextComp - Text Component with i18n Support:
// Usage <TextComp text="WELCOME" /> <TextComp isDynamic text="Hello World" /> // Direct text without translation
ButtonComp - Customizable Button:
// Usage
<ButtonComp
text="LOGIN"
onPress={handleLogin}
variant="primary" // or "secondary", "outline", etc.
isLoading={isLoading}
/>TextInputComp - Custom Text Input:
// Usage
<TextInputComp
label="EMAIL"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
isPassword={false}
/>HeaderComp - App Header:
// Usage
<HeaderComp
title="PROFILE"
showBack={true}
onBackPress={() => navigation.goBack()}
/>WrapperContainer - Safe Area Wrapper:
// Usage
<WrapperContainer>
{/* Screen content */}
</WrapperContainer>The app uses Redux Toolkit for centralized state management:
// Store setup
// src/redux/store.ts
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;Reducers Structure:
// src/redux/reducers/index.ts
import { combineReducers } from '@reduxjs/toolkit';
import authReducer from './authSlice';
import appReducer from './appSlice';
const rootReducer = combineReducers({
auth: authReducer,
app: appReducer,
});
export default rootReducer;Actions Structure:
// src/redux/actions/index.ts
import * as authActions from './authActions';
import * as appActions from './appActions';
export default {
...authActions,
...appActions,
};Using Redux in Components:
// In a component
import { useDispatch, useSelector } from 'react-redux';
import actions from '@/redux/actions';
import { RootState } from '@/redux/store';
const Component = () => {
const dispatch = useDispatch();
const { user } = useSelector((state: RootState) => state.auth);
const handleLogin = async () => {
await dispatch(actions.login(credentials));
};
return (/* Component JSX */);
};The app uses a centralized API service based on Axios:
// src/config/api.ts
import axios from 'axios';
import { API_BASE_URL } from '@/config/constants';
const apiInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor
apiInstance.interceptors.request.use(
async (config) => {
// Add authorization token if available
const token = await getAuthToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor
apiInstance.interceptors.response.use(
(response) => response.data,
(error) => {
// Handle errors (401, 403, 500, etc.)
return Promise.reject(error);
}
);
export default apiInstance;API Actions:
// src/redux/actions/authActions.ts
import apiInstance from '@/config/api';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const login = createAsyncThunk(
'auth/login',
async (credentials, { rejectWithValue }) => {
try {
const response = await apiInstance.post('/auth/login', credentials);
return response;
} catch (error) {
return rejectWithValue(error.response?.data || 'Login failed');
}
}
);The app uses Expo's SecureStore for encrypted storage:
// src/utils/secureStorage.ts
import * as SecureStore from 'expo-secure-store';
// Storage keys
export const STORAGE_KEYS = {
AUTH_TOKEN: 'AUTH_TOKEN',
USER_DATA: 'USER_DATA',
LANGUAGE: 'LANGUAGE',
THEME: 'THEME',
} as const;
type StorageKeyType = keyof typeof STORAGE_KEYS;
export const secureStorage = {
async setItem(key: StorageKeyType, value: string) {
try {
await SecureStore.setItemAsync(STORAGE_KEYS[key], value);
} catch (error) {
console.error('Error storing value:', error);
}
},
async getItem(key: StorageKeyType) {
try {
return await SecureStore.getItemAsync(STORAGE_KEYS[key]);
} catch (error) {
console.error('Error retrieving value:', error);
return null;
}
},
async removeItem(key: StorageKeyType) {
try {
await SecureStore.deleteItemAsync(STORAGE_KEYS[key]);
} catch (error) {
console.error('Error removing value:', error);
}
},
// Helper for storing objects
async setObject(key: StorageKeyType, value: object) {
try {
const jsonValue = JSON.stringify(value);
await this.setItem(key, jsonValue);
} catch (error) {
console.error('Error storing object:', error);
}
},
// Helper for retrieving objects
async getObject(key: StorageKeyType) {
try {
const jsonValue = await this.getItem(key);
return jsonValue ? JSON.parse(jsonValue) : null;
} catch (error) {
console.error('Error retrieving object:', error);
return null;
}
},
};The app uses react-native-svg and react-native-svg-transformer for SVG support:
// In components
import Logo from '@/assets/icons/logo.svg';
const Component = () => {
return (
<View>
<Logo width={100} height={100} fill={colors.primary} />
</View>
);
};The app uses a scaling utility for responsive dimensions:
// src/styles/scaling.ts
import { Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window');
// Guideline sizes are based on standard ~5" screen mobile device
const guidelineBaseWidth = 350;
const guidelineBaseHeight = 680;
export const horizontalScale = (size: number) => (width / guidelineBaseWidth) * size;
export const verticalScale = (size: number) => (height / guidelineBaseHeight) * size;
export const moderateScale = (size: number, factor = 0.5) =>
size + (horizontalScale(size) - size) * factor;Using Scale in Styles:
import { moderateScale } from '@/styles/scaling';
const styles = StyleSheet.create({
container: {
padding: moderateScale(16),
marginBottom: moderateScale(20),
},
title: {
fontSize: moderateScale(18),
},
});The app uses React Navigation 7.x with a structured approach:
// src/navigation/Routes.tsx
import React, { useEffect, useState } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import AuthStack from './stacks/AuthStack';
import MainStack from './stacks/MainStack';
import { useSelector } from 'react-redux';
import { RootState } from '@/redux/store';
const Routes = () => {
const { isAuthenticated } = useSelector((state: RootState) => state.auth);
return (
<NavigationContainer>
{isAuthenticated ? <MainStack /> : <AuthStack />}
</NavigationContainer>
);
};
export default Routes;Navigation Types:
// src/navigation/types.ts
export type AuthStackParamList = {
Login: undefined;
Signup: undefined;
OTPVerification: { email: string };
};
export type MainStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
};
export type TabParamList = {
HomeTab: undefined;
ProfileTab: undefined;
SettingsTab: undefined;
};This modular approach separates concerns clearly, making the codebase more maintainable:
The architecture is designed to scale with your application:
TypeScript provides: