NativeCraft is a React Native mobile application built with TypeScript and Expo. It features 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
Start the development server
npm start # or yarn start
Run on specific platform
# iOS npm run ios # Android npm run android
The 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: