Redux Implementation and Purpose in React Native
Centralized State Management
Predictable Data Flow
Enhanced Debugging
Improved Code Maintainability
Better Team Collaboration
How to Setting Up Redux
How to Creating the Store(how the state changes in response to actions)
How to Action Types and Actions
How to Connecting Redux to React Native(Wrap your main component with the Provider to make the store available)
Connect components to Redux using hooks:
how the state changes in response to actions
When to Use Redux
When Not to Use Redux
what is slice and its benefit
Difference between reducers and extrareducers
When to Use extraReducers
How to State Handling for Different Async States using extraReducers
Listout real time application of reducers
Redux Implementation and Purpose in React Native
Redux is a predictable state management library that provides a robust solution for managing application state in React Native apps. It centralizes state management, creating a more maintainable and predictable codebase, especially for complex applications.
Purpose of Redux in React Native
Centralized State Management
Redux creates a single "store" that holds the entire application state, making data accessible to any component without complex prop drilling. This central store acts as a single source of truth, which is particularly valuable in larger applications where data needs to be shared across multiple screens and components.
Predictable Data Flow
Redux implements a unidirectional data flow that makes state changes more predictable and easier to track. This predictability comes from:
State can only change through dispatched actions
Reducers are pure functions that produce a new state based on the previous state and action
Changes occur in a strict, deterministic order
Enhanced Debugging
Redux provides powerful developer tools that allow real-time monitoring of:
State changes
Action dispatches
State history (time-travel debugging)
This makes troubleshooting and development significantly easier, as you can inspect exactly how and when your application state changes.
Improved Code Maintainability
By separating state management logic from UI components, Redux creates a cleaner architecture where concerns are properly separated. This separation makes the codebase more maintainable as it grows, simplifying testing and feature development.
Better Team Collaboration
The strict structure and predictable patterns in Redux make it easier for multiple developers to work on the same project without conflicts. The clear action-reducer-state pattern provides a common language for discussing application behavior.
Redux Implementation in React Native
-
Setting Up Redux
First, install the necessary packages:
npm install redux react-redux
For modern Redux development, also consider Redux Toolkit:
npm install @reduxjs/toolkit
-
Creating the Store
The store is the central piece that holds the application state:
import { createStore } from 'redux';
// Define the initial state
const initialState = {
counter: 0
};
// Create a reducer
function reducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, counter: state.counter + 1 };
case 'DECREMENT':
return { ...state, counter: state.counter - 1 };
default:
return state;
}
}
// Create and export the store
const store = createStore(reducer);
export default store;
-
Organizing Redux with a Folder Structure
For larger applications, organize Redux code with a clear structure:
src/
├── redux/
│ ├── actions/
│ │ └── counterActions.js
│ ├── constants/
│ │ └── counterActionTypes.js
│ ├── reducers/
│ │ └── counterReducer.js
│ └── store.js
Action Types and Actions
Define action types as constants to avoid typos and improve maintainability:
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
// counterActions.js
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../constants/counterActionTypes';
export const incrementCounter = (value = 1) => ({
type: INCREMENT_COUNTER,
payload: value
});
export const decrementCounter = () => ({
type: DECREMENT_COUNTER
});
Creating Reducers
Reducers specify how the state changes in response to actions:
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../constants/counterActionTypes';
const initialState = {
counter: 0
};
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT_COUNTER:
return { ...state, counter: state.counter + action.payload };
case DECREMENT_COUNTER:
return { ...state, counter: state.counter - 1 };
default:
return state;
}
};
export default counterReducer;
Connecting Redux to React Native
Wrap your main component with the Provider to make the store available:
import React from 'react';
import { Provider } from 'react-redux';
import store from './redux/store';
import App from './App';
const Root = () => (
<Provider store={store}>
<App />
</Provider>
);
export default Root;
Using Redux in Components
Connect components to Redux using hooks:
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { incrementCounter, decrementCounter } from './redux/actions/counterActions';
const CounterScreen = () => {
// Get state from Redux store
const counter = useSelector(state => state.counter);
// Get dispatch function
const dispatch = useDispatch();
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 24 }}>Counter: {counter}</Text>
<Button
title="Increment"
onPress={() => dispatch(incrementCounter())}
/>
<Button
title="Decrement"
onPress={() => dispatch(decrementCounter())}
/>
</View>
);
};
export default CounterScreen;
Modern Approach with Redux Toolkit
Redux Toolkit simplifies Redux implementation:
import { createSlice, configureStore } from '@reduxjs/toolkit';
// Create a slice
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
}
}
});
// Export actions
export const { increment, decrement } = counterSlice.actions;
// Create and export store
const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
export default store;
When to Use Redux
- For medium to large applications with complex state management needs
- When state needs to be shared across multiple components or screens
- When you need predictable state updates and debugging capabilities
- When multiple team members need to work on state management in a structured way
When Not to Use Redux
- For simple applications with minimal state requirements
- When component local state is sufficient
- For very small teams or solo developers building simple apps
Redux provides significant benefits for React Native applications by centralizing state management, making data flow predictable, and improving debugging capabilities. While it adds some complexity to your project setup, these benefits become increasingly valuable as your application grows in size and complexity.
Redux Toolkit provides powerful abstractions to simplify Redux state management. Let's explore three core concepts that work together to handle complex state logic, particularly for asynchronous operations.
Slices
A Redux slice is a collection of reducer logic and actions related to a specific feature or domain within your application's state.
What is a Slice?
A slice represents a portion of your Redux store, typically organized around a specific feature (e.g., users, posts, authentication).
It combines reducers, action creators, and action types in a single, cohesive unit.
Using createSlice
Redux Toolkit's createSlice function automatically generates action creators and action types based on the reducer functions you provide:
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
}
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
Benefits of Slices
- Reduces boilerplate code significantly
- Enforces best practices like immutable updates (via Immer)
- Improves code organization by feature domain
- Automatically generates action creators and types
ExtraReducers
While reducers handle actions created within a slice, extraReducers allow a slice to respond to actions defined elsewhere.
Key Differences from Regular Reducers
No Action Generation
: extraReducers respond to external actions but don't generate new action creators.
External Action Handling
: Used for actions defined in other slices or by createAsyncThunk.
Shared Action Response
: Allows multiple reducers to respond to the same action.
When to Use extraReducers
Handling async thunk actions (pending, fulfilled, rejected states)
Responding to actions from other slices
Handling global actions (like user logout) that affect multiple slices
The modern approach uses a "builder callback" pattern:
extraReducers: (builder) => {
builder
.addCase(fetchVehicles.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchVehicles.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchVehicles.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
CreateAsyncThunk
createAsyncThunk simplifies handling asynchronous operations in Redux by generating thunks that dispatch standardized actions.
How It Works
Creates a thunk action creator for async operations
Automatically dispatches lifecycle actions: pending, fulfilled, and rejected
Manages promise-based operations like API calls
Parameters
Action Type String: Used as a prefix for generated action types (e.g., 'users/fetchByIdStatus')
Payload Creator: Async callback that returns a promise
Options Object: Optional configuration settings
Generated Actions
For an action type 'posts/getPosts', createAsyncThunk generates three action types:
posts/getPosts/pending: Dispatched when the async operation starts
posts/getPosts/fulfilled: Dispatched when the operation succeeds
posts/getPosts/rejected: Dispatched when the operation fails
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
// Create the async thunk
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async () => {
const response = await fetch('https://api.example.com/posts');
return response.json();
}
);
// Create a slice with extraReducers to handle the async thunk
const postsSlice = createSlice({
name: 'posts',
initialState: {
entities: [],
loading: false,
error: null
},
reducers: {
// Local synchronous reducers go here
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.loading = true;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = false;
state.entities = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
export default postsSlice.reducer;
How These Concepts Work Together
Create async thunks using createAsyncThunk for API calls and other async operations.
Define a slice with createSlice to organize your feature's state management.
Use extraReducers to respond to the async thunk's lifecycle actions (pending/fulfilled/rejected).
This pattern creates a clean, efficient workflow for handling async operations in Redux, significantly reducing boilerplate and improving maintainability of your application state.
How to State Handling for Different Async States using extraReducers
extraReducers: (builder) => {
builder
.addCase(fetchVehicles.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchVehicles.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchVehicles.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
This handles the three states of the async operation:
Pending
: When the request starts
Fulfilled
: When data is successfully received
Rejected
: When an error occurs
Part 2: Store Configuration
const store = configureStore({
reducer: {
country: countryReducer,
otp: otpReducer,
vehicles: vehicleReducer, // Key relationship here
},
});
Practical Example
extraReducers: (builder) => {
builder
.addCase(fetchVehicles.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchVehicles.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchVehicles.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
User Interface Synchronization
frontend implemention
function VehicleListScreen() {
const { data: vehicles, loading, error } = useSelector(state => state.vehicles);
// Show loading spinner when fetching
if (loading) return <LoadingSpinner />;
// Show error message if request failed
if (error) return <ErrorMessage message={error} />;
// Render vehicles when data is available
return (
<FlatList
data={vehicles}
renderItem={({item}) => <VehicleCard vehicle={item} />}
keyExtractor={item => item.id.toString()}
/>
);
}
reducers: {
clearVehicles: (state) => {
state.data = [];
}
}
frontend implemention
const handleClearVehicles = () => {
dispatch(clearVehicles());
};
reducers: {
resetOtpState: (state) => {
state.loading = false;
state.sendSuccess = false;
state.verifySuccess = false;
state.error = null;
}
},
Another Practical Example
extraReducers: (builder) => {
builder
.addCase(fetchVehicles.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchVehicles.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchVehicles.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
const vehicles = vehiclesState.data || [];
const loading = vehiclesState.loading || false;
const error = vehiclesState.error || null;
if (loading) {
return (
<View style={[styles.container, styles.centered]}>
<ActivityIndicator size="large" color="#FF5733" />
<Text>Loading vehicles...</Text>
</View>
);
}
Listout real time application of reducers
User Interface Synchronization
function VehicleListScreen() {
const { data: vehicles, loading, error } = useSelector(state => state.vehicles);
// Show loading spinner when fetching
if (loading) return <LoadingSpinner />;
// Show error message if request failed
if (error) return <ErrorMessage message={error} />;
// Render vehicles when data is available
return (
<FlatList
data={vehicles}
renderItem={({item}) => <VehicleCard vehicle={item} />}
keyExtractor={item => item.id.toString()}
/>
);
}
- Pull-to-Refresh Implementation
function RefreshableVehicleList() {
const dispatch = useDispatch();
const { baseUrl } = useBaseUrl();
const { loading } = useSelector(state => state.vehicles);
const handleRefresh = () => {
dispatch(fetchVehicles({ baseUrl }));
};
return (
<FlatList
refreshing={loading}
onRefresh={handleRefresh}
// other props
/>
);
}
- Filter Implementation
function FilterableVehicleList() {
const dispatch = useDispatch();
const [filters, setFilters] = useState({});
// Apply new filters and fetch filtered data
const applyFilters = (newFilters) => {
setFilters(newFilters);
dispatch(clearVehicles()); // Clear existing data
dispatch(fetchVehicles({ baseUrl, filters: newFilters }));
};
// Component JSX
}
- Error Retry Logic
function VehicleListWithRetry() {
const dispatch = useDispatch();
const { error } = useSelector(state => state.vehicles);
return (
<>
{error && (
<RetryButton
onPress={() => dispatch(fetchVehicles({ baseUrl }))}
message="Failed to load vehicles. Tap to retry."
/>
)}
{/* Rest of component */}
</>
);
}
Vehicle Filtering and Search Functionality
reducers: {
filterCountries(state, action) {
const searchTerm = action.payload.toLowerCase();
state.filteredCountries = state.countries.filter((country) =>
country.name.toLowerCase().includes(searchTerm)
);
},
},
frontend implemention
const handleSearchChange = (text) => {
setSearchTerm(text); // Update local search term state
dispatch(filterCountries(text)); // Dispatch Redux action to filter countries
};
const vehicleSlice = createSlice({
name: 'vehicles',
initialState: {
loading: false,
data: [],
error: null,
filters: {
priceRange: [0, 10000],
brands: [],
availability: null
},
filteredResults: []
},
reducers: {
updateFilters: (state, action) => {
state.filters = {...state.filters, ...action.payload};
state.filteredResults = state.data.filter(vehicle =>
filterVehicle(vehicle, state.filters)
);
}
}
});
frontend implemention
const VehicleSearchScreen = () => {
const dispatch = useDispatch();
const { data: vehicles, filters, filteredResults, loading } = useSelector(state => state.vehicles);
const [localFilters, setLocalFilters] = useState(filters);
// Apply filters to Redux state when user submits
const applyFilters = () => {
dispatch(updateFilters(localFilters));
};
return (
<View style={styles.container}>
{/* Filter Controls */}
<View style={styles.filterSection}>
<Text style={styles.sectionTitle}>Filters</Text>
{/* Price Range Slider */}
<Text>Price Range: ${localFilters.priceRange[0]} - ${localFilters.priceRange[1]}</Text>
<Slider
values={localFilters.priceRange}
min={0}
max={10000}
onValuesChange={(values) => setLocalFilters({...localFilters, priceRange: values})}
/>
{/* Brand Selection */}
<Text>Select Brands:</Text>
<FlatList
data={['Toyota', 'Honda', 'BMW', 'Mercedes']}
renderItem={({item}) => (
<CheckBox
title={item}
checked={localFilters.brands.includes(item)}
onPress={() => {
const newBrands = localFilters.brands.includes(item)
? localFilters.brands.filter(brand => brand !== item)
: [...localFilters.brands, item];
setLocalFilters({...localFilters, brands: newBrands});
}}
/>
)}
keyExtractor={item => item}
/>
{/* Apply Button */}
<TouchableOpacity
style={styles.applyButton}
onPress={applyFilters}
>
<Text style={styles.buttonText}>Apply Filters</Text>
</TouchableOpacity>
</View>
{/* Results Section */}
{loading ? (
<ActivityIndicator size="large" color="#FF5733" />
) : (
<FlatList
data={filteredResults.length > 0 ? filteredResults : vehicles}
renderItem={({item}) => <VehicleCard vehicle={item} />}
keyExtractor={item => item.id.toString()}
/>
)}
</View>
);
};
Booking Management System
const bookingSlice = createSlice({
name: 'bookings',
initialState: {
activeBookings: [],
pastBookings: [],
pendingBookings: [],
loading: false,
error: null
},
reducers: {
acceptBooking: (state, action) => {
const bookingId = action.payload;
const bookingIndex = state.pendingBookings.findIndex(b => b.id === bookingId);
if (bookingIndex >= 0) {
const booking = state.pendingBookings[bookingIndex];
state.activeBookings.push({...booking, status: 'active'});
state.pendingBookings.splice(bookingIndex, 1);
}
}
}
});
frontend implemention
const BookingManagementScreen = () => {
const dispatch = useDispatch();
const { activeBookings, pendingBookings, pastBookings, loading } = useSelector(state => state.bookings);
const [selectedTab, setSelectedTab] = useState('active');
useEffect(() => {
dispatch(fetchBookings());
}, []);
const handleAcceptBooking = (bookingId) => {
dispatch(acceptBooking(bookingId))
.then(() => {
Alert.alert('Success', 'Booking accepted successfully');
})
.catch(error => {
Alert.alert('Error', error.message);
});
};
const renderBookingItem = (booking) => (
<View style={styles.bookingCard}>
<Text style={styles.vehicleInfo}>{booking.vehicleName}</Text>
<Text>Booking Date: {new Date(booking.bookingDate).toLocaleDateString()}</Text>
<Text>Duration: {booking.duration} hours</Text>
<Text>Status: <Text style={styles[booking.status]}>{booking.status}</Text></Text>
{booking.status === 'pending' && (
<View style={styles.actionButtons}>
<TouchableOpacity
style={styles.acceptButton}
onPress={() => handleAcceptBooking(booking.id)}
>
<Text style={styles.buttonText}>Accept</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.rejectButton}
onPress={() => dispatch(rejectBooking(booking.id))}
>
<Text style={styles.buttonText}>Reject</Text>
</TouchableOpacity>
</View>
)}
</View>
);
return (
<View style={styles.container}>
{/* Tab Navigation */}
<View style={styles.tabBar}>
{['active', 'pending', 'past'].map(tab => (
<TouchableOpacity
key={tab}
style={[styles.tab, selectedTab === tab && styles.activeTab]}
onPress={() => setSelectedTab(tab)}
>
<Text style={styles.tabText}>{tab.charAt(0).toUpperCase() + tab.slice(1)}</Text>
</TouchableOpacity>
))}
</View>
{/* Booking List based on Selected Tab */}
{loading ? (
<ActivityIndicator size="large" color="#FF5733" />
) : (
<FlatList
data={
selectedTab === 'active' ? activeBookings :
selectedTab === 'pending' ? pendingBookings :
pastBookings
}
renderItem={({item}) => renderBookingItem(item)}
keyExtractor={item => item.id.toString()}
ListEmptyComponent={<Text style={styles.emptyText}>No {selectedTab} bookings</Text>}
/>
)}
</View>
);
};
User Ratings and Reviews System
const ratingsSlice = createSlice({
name: 'ratings',
initialState: {
vehicleRatings: {},
userRatings: {},
loading: false
},
reducers: {
submitRating: (state, action) => {
const { vehicleId, rating, review } = action.payload;
if (!state.vehicleRatings[vehicleId]) {
state.vehicleRatings[vehicleId] = [];
}
state.vehicleRatings[vehicleId].push({ rating, review, date: new Date().toISOString() });
}
}
});
frontend implemention
const RatingsReviewScreen = () => {
const dispatch = useDispatch();
const { vehicleId } = useRoute().params;
const { vehicleRatings, loading } = useSelector(state => state.ratings);
const [rating, setRating] = useState(0);
const [review, setReview] = useState('');
// Get ratings for this specific vehicle
const vehicleSpecificRatings = vehicleRatings[vehicleId] || [];
const averageRating = vehicleSpecificRatings.length > 0
? vehicleSpecificRatings.reduce((sum, item) => sum + item.rating, 0) / vehicleSpecificRatings.length
: 0;
const handleSubmitRating = () => {
if (rating === 0) {
Alert.alert('Error', 'Please select a rating');
return;
}
dispatch(submitRating({
vehicleId,
rating,
review
}))
.then(() => {
Alert.alert('Success', 'Rating submitted successfully');
setRating(0);
setReview('');
})
.catch(error => {
Alert.alert('Error', error.message);
});
};
return (
<View style={styles.container}>
{/* Average Rating Display */}
<View style={styles.averageRatingContainer}>
<Text style={styles.averageRatingText}>{averageRating.toFixed(1)}</Text>
<StarRating
disabled={true}
maxStars={5}
rating={averageRating}
fullStarColor="#FFD700"
/>
<Text style={styles.reviewCountText}>
{vehicleSpecificRatings.length} {vehicleSpecificRatings.length === 1 ? 'review' : 'reviews'}
</Text>
</View>
{/* Submit New Rating */}
<View style={styles.submitRatingContainer}>
<Text style={styles.sectionTitle}>Write a Review</Text>
<StarRating
disabled={false}
maxStars={5}
rating={rating}
selectedStar={(rating) => setRating(rating)}
fullStarColor="#FFD700"
/>
<TextInput
style={styles.reviewInput}
placeholder="Share your experience..."
value={review}
onChangeText={setReview}
multiline
/>
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmitRating}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Submitting...' : 'Submit Review'}
</Text>
</TouchableOpacity>
</View>
{/* Past Reviews */}
<Text style={styles.sectionTitle}>Reviews</Text>
<FlatList
data={vehicleSpecificRatings}
renderItem={({item}) => (
<View style={styles.reviewCard}>
<View style={styles.reviewHeader}>
<StarRating
disabled={true}
maxStars={5}
rating={item.rating}
starSize={18}
fullStarColor="#FFD700"
/>
<Text style={styles.reviewDate}>
{new Date(item.date).toLocaleDateString()}
</Text>
</View>
<Text style={styles.reviewText}>{item.review}</Text>
</View>
)}
keyExtractor={(item, index) => index.toString()}
ListEmptyComponent={<Text style={styles.emptyText}>No reviews yet</Text>}
/>
</View>
);
};
Location-Based Vehicle Search
const locationSlice = createSlice({
name: 'location',
initialState: {
userLocation: null,
nearbyVehicles: [],
searchRadius: 5, // kilometers
loading: false
},
reducers: {
setUserLocation: (state, action) => {
state.userLocation = action.payload;
},
setSearchRadius: (state, action) => {
state.searchRadius = action.payload;
}
},
extraReducers: (builder) => {
builder
.addCase(searchNearbyVehicles.pending, (state) => {
state.loading = true;
})
.addCase(searchNearbyVehicles.fulfilled, (state, action) => {
state.loading = false;
state.nearbyVehicles = action.payload;
});
}
});
frontend implemention
const NearbyVehiclesScreen = () => {
const dispatch = useDispatch();
const { userLocation, nearbyVehicles, searchRadius, loading } = useSelector(state => state.location);
const { baseUrl } = useBaseUrl();
useEffect(() => {
// Get user's current location
getCurrentPosition()
.then(position => {
const { latitude, longitude } = position.coords;
dispatch(setUserLocation({ latitude, longitude }));
// Search for nearby vehicles once location is obtained
if (baseUrl) {
dispatch(searchNearbyVehicles({
baseUrl,
latitude,
longitude,
radius: searchRadius
}));
}
})
.catch(error => {
Alert.alert('Error', 'Unable to get your location. Please enable location services.');
});
}, [baseUrl]);
// Search with updated radius
const updateSearchRadius = (radius) => {
dispatch(setSearchRadius(radius));
if (userLocation) {
dispatch(searchNearbyVehicles({
baseUrl,
latitude: userLocation.latitude,
longitude: userLocation.longitude,
radius
}));
}
};
return (
<View style={styles.container}>
{/* Map View */}
<View style={styles.mapContainer}>
{userLocation ? (
<MapView
style={styles.map}
initialRegion={{
latitude: userLocation.latitude,
longitude: userLocation.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
>
{/* User Location Marker */}
<Marker
coordinate={userLocation}
title="Your Location"
pinColor="blue"
/>
{/* Radius Circle */}
<Circle
center={userLocation}
radius={searchRadius * 1000} // Convert km to meters
strokeWidth={1}
strokeColor="rgba(66, 133, 244, 0.5)"
fillColor="rgba(66, 133, 244, 0.1)"
/>
{/* Vehicle Markers */}
{nearbyVehicles.map(vehicle => (
<Marker
key={vehicle.id}
coordinate={{
latitude: vehicle.latitude,
longitude: vehicle.longitude
}}
title={`${vehicle.brand} ${vehicle.model}`}
description={`$${vehicle.price}/hour`}
onCalloutPress={() => navigation.navigate('VehicleDetails', { vehicle })}
/>
))}
</MapView>
) : (
<ActivityIndicator size="large" color="#FF5733" />
)}
</View>
{/* Search Radius Control */}
<View style={styles.radiusControl}>
<Text style={styles.radiusLabel}>Search Radius: {searchRadius} km</Text>
<Slider
value={searchRadius}
minimumValue={1}
maximumValue={20}
step={1}
onValueChange={updateSearchRadius}
/>
</View>
{/* Nearby Vehicles List */}
<View style={styles.listContainer}>
<Text style={styles.sectionTitle}>
{loading ? 'Searching...' : `${nearbyVehicles.length} Vehicles Nearby`}
</Text>
<FlatList
data={nearbyVehicles}
renderItem={({item}) => (
<TouchableOpacity
style={styles.vehicleCard}
onPress={() => navigation.navigate('VehicleDetails', { vehicle: item })}
>
<Image source={{ uri: item.image }} style={styles.vehicleImage} />
<View style={styles.vehicleInfo}>
<Text style={styles.vehicleName}>{item.brand} {item.model}</Text>
<Text style={styles.vehiclePrice}>${item.price}/hour</Text>
<Text style={styles.vehicleDistance}>
{calculateDistance(
userLocation.latitude,
userLocation.longitude,
item.latitude,
item.longitude
).toFixed(1)} km away
</Text>
</View>
</TouchableOpacity>
)}
keyExtractor={item => item.id.toString()}
/>
</View>
</View>
);
};
Payment Processing System
const paymentSlice = createSlice({
name: 'payments',
initialState: {
paymentMethods: [],
transactions: [],
pendingPayment: null,
processingPayment: false,
error: null
},
reducers: {
addPaymentMethod: (state, action) => {
state.paymentMethods.push(action.payload);
},
selectPaymentMethod: (state, action) => {
state.pendingPayment = {
...state.pendingPayment,
paymentMethodId: action.payload
};
}
},
extraReducers: (builder) => {
builder
.addCase(processPayment.pending, (state) => {
state.processingPayment = true;
state.error = null;
})
.addCase(processPayment.fulfilled, (state, action) => {
state.processingPayment = false;
state.transactions.push(action.payload);
state.pendingPayment = null;
})
.addCase(processPayment.rejected, (state, action) => {
state.processingPayment = false;
state.error = action.payload;
});
}
});
frontend implemention
const PaymentScreen = () => {
const dispatch = useDispatch();
const { booking } = useRoute().params;
const { paymentMethods, processingPayment, pendingPayment, error } = useSelector(state => state.payments);
const [selectedPaymentId, setSelectedPaymentId] = useState(null);
useEffect(() => {
dispatch(fetchPaymentMethods());
// Create a pending payment record
dispatch(createPendingPayment({
bookingId: booking.id,
amount: booking.totalAmount,
description: `Booking for ${booking.vehicleName}`,
currency: 'USD'
}));
}, [booking]);
const handlePayment = () => {
if (!selectedPaymentId) {
Alert.alert('Error', 'Please select a payment method');
return;
}
dispatch(selectPaymentMethod(selectedPaymentId));
// Process payment with selected method
dispatch(processPayment())
.then(() => {
Alert.alert('Success', 'Payment processed successfully');
navigation.navigate('BookingConfirmation', { bookingId: booking.id });
})
.catch(error => {
Alert.alert('Payment Failed', error.message);
});
};
// Add a new payment method
const handleAddPaymentMethod = () => {
navigation.navigate('AddPaymentMethod', {
onPaymentAdded: (newMethod) => {
dispatch(addPaymentMethod(newMethod));
setSelectedPaymentId(newMethod.id);
}
});
};
return (
<View style={styles.container}>
{/* Booking Summary */}
<View style={styles.bookingSummary}>
<Text style={styles.sectionTitle}>Booking Summary</Text>
<Text style={styles.vehicleName}>{booking.vehicleName}</Text>
<Text>Start: {new Date(booking.startTime).toLocaleString()}</Text>
<Text>End: {new Date(booking.endTime).toLocaleString()}</Text>
<Text>Duration: {booking.duration} hours</Text>
<View style={styles.priceSummary}>
<Text style={styles.priceLabel}>Total:</Text>
<Text style={styles.priceValue}>${booking.totalAmount.toFixed(2)}</Text>
</View>
</View>
{/* Payment Method Selection */}
<View style={styles.paymentSelection}>
<Text style={styles.sectionTitle}>Select Payment Method</Text>
{paymentMethods.length > 0 ? (
<FlatList
data={paymentMethods}
renderItem={({item}) => (
<TouchableOpacity
style={[
styles.paymentMethod,
selectedPaymentId === item.id && styles.selectedPaymentMethod
]}
onPress={() => setSelectedPaymentId(item.id)}
>
<Image source={getPaymentIcon(item.type)} style={styles.paymentIcon} />
<View style={styles.paymentDetails}>
<Text style={styles.paymentName}>{item.name}</Text>
<Text style={styles.paymentInfo}>
{item.type === 'card'
? `**** **** **** ${item.lastFour}`
: item.email}
</Text>
</View>
{selectedPaymentId === item.id && (
<Icon name="check-circle" size={24} color="#4CAF50" />
)}
</TouchableOpacity>
)}
keyExtractor={item => item.id.toString()}
/>
) : (
<Text style={styles.emptyText}>No payment methods available</Text>
)}
<TouchableOpacity
style={styles.addPaymentButton}
onPress={handleAddPaymentMethod}
>
<Icon name="plus" size={16} color="#FFF" />
<Text style={styles.buttonText}>Add Payment Method</Text>
</TouchableOpacity>
</View>
{/* Error Message */}
{error && (
<Text style={styles.errorText}>{error}</Text>
)}
{/* Pay Button */}
<TouchableOpacity
style={[styles.payButton, (!selectedPaymentId || processingPayment) && styles.disabledButton]}
onPress={handlePayment}
disabled={!selectedPaymentId || processingPayment}
>
<Text style={styles.buttonText}>
{processingPayment ? 'Processing...' : `Pay $${booking.totalAmount.toFixed(2)}`}
</Text>
</TouchableOpacity>
</View>
);
};
User Notification System
const notificationSlice = createSlice({
name: 'notifications',
initialState: {
notifications: [],
unread: 0,
settings: {
pushEnabled: true,
emailEnabled: true,
bookingAlerts: true,
promotionalAlerts: false
}
},
reducers: {
addNotification: (state, action) => {
state.notifications.unshift(action.payload);
state.unread += 1;
},
markAsRead: (state, action) => {
const id = action.payload;
const notification = state.notifications.find(n => n.id === id);
if (notification && !notification.read) {
notification.read = true;
state.unread = Math.max(0, state.unread - 1);
}
},
updateSettings: (state, action) => {
state.settings = {...state.settings, ...action.payload};
}
}
});
frontend implemention
const NotificationsScreen = () => {
const dispatch = useDispatch();
const { notifications, unread, settings } = useSelector(state => state.notifications);
useEffect(() => {
dispatch(fetchNotifications());
}, []);
const handleMarkAsRead = (notificationId) => {
dispatch(markAsRead(notificationId));
};
const handleMarkAllAsRead = () => {
dispatch(markAllAsRead());
};
const handleSettingToggle = (setting) => {
dispatch(updateSettings({
[setting]: !settings[setting]
}));
};
const renderNotification = (notification) => {
const isUnread = !notification.read;
return (
<TouchableOpacity
style={[styles.notificationItem, isUnread && styles.unreadNotification]}
onPress={() => handleMarkAsRead(notification.id)}
>
<View style={styles.notificationIcon}>
{getNotificationIcon(notification.type)}
</View>
<View style={styles.notificationContent}>
<Text style={[styles.notificationTitle, isUnread && styles.boldText]}>
{notification.title}
</Text>
<Text style={styles.notificationMessage}>{notification.message}</Text>
<Text style={styles.notificationTime}>
{formatTimeAgo(notification.timestamp)}
</Text>
</View>
{isUnread && <View style={styles.unreadIndicator} />}
</TouchableOpacity>
);
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.screenTitle}>Notifications</Text>
{unread > 0 && (
<TouchableOpacity style={styles.markAllButton} onPress={handleMarkAllAsRead}>
<Text style={styles.markAllText}>Mark all as read</Text>
</TouchableOpacity>
)}
</View>
{/* Notification List */}
<FlatList
data={notifications}
renderItem={({item}) => renderNotification(item)}
keyExtractor={item => item.id.toString()}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Icon name="bell-slash" size={48} color="#ccc" />
<Text style={styles.emptyText}>No notifications yet</Text>
</View>
}
/>
{/* Notification Settings */}
<View style={styles.settingsContainer}>
<Text style={styles.sectionTitle}>Notification Settings</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Push Notifications</Text>
<Switch
value={settings.pushEnabled}
onValueChange={() => handleSettingToggle('pushEnabled')}
/>
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Email Notifications</Text>
<Switch
value={settings.emailEnabled}
onValueChange={() => handleSettingToggle('emailEnabled')}
/>
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Booking Alerts</Text>
<Switch
value={settings.bookingAlerts}
onValueChange={() => handleSettingToggle('bookingAlerts')}
/>
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>Promotional Alerts</Text>
<Switch
value={settings.promotionalAlerts}
onValueChange={() => handleSettingToggle('promotionalAlerts')}
/>
</View>
</View>
</View>
);
};
Vehicle Maintenance Tracking
const maintenanceSlice = createSlice({
name: 'maintenance',
initialState: {
records: {},
scheduledMaintenance: [],
loading: false
},
reducers: {
addMaintenanceRecord: (state, action) => {
const { vehicleId, record } = action.payload;
if (!state.records[vehicleId]) {
state.records[vehicleId] = [];
}
state.records[vehicleId].push({
...record,
date: new Date().toISOString()
});
},
scheduleMaintenanceAlert: (state, action) => {
state.scheduledMaintenance.push(action.payload);
}
}
});
frontend implemention
const MaintenanceScreen = () => {
const dispatch = useDispatch();
const { vehicleId } = useRoute().params;
const { records, scheduledMaintenance, loading } = useSelector(state => state.maintenance);
const [maintenanceType, setMaintenanceType] = useState('');
const [notes, setNotes] = useState('');
const [showAddForm, setShowAddForm] = useState(false);
useEffect(() => {
dispatch(fetchMaintenanceRecords(vehicleId));
dispatch(fetchScheduledMaintenance(vehicleId));
}, [vehicleId]);
const vehicleRecords = records[vehicleId] || [];
const handleAddRecord = () => {
if (!maintenanceType) {
Alert.alert('Error', 'Please select maintenance type');
return;
}
dispatch(addMaintenanceRecord({
vehicleId,
record: {
type: maintenanceType,
notes,
timestamp: new Date().toISOString()
}
}));
// Reset form
setMaintenanceType('');
setNotes('');
setShowAddForm(false);
};
const scheduleNextMaintenance = (type, daysFromNow) => {
const date = new Date();
date.setDate(date.getDate() + daysFromNow);
dispatch(scheduleMaintenanceAlert({
vehicleId,
type,
scheduledDate: date.toISOString()
}));
Alert.alert('Success', `${type} scheduled for ${date.toLocaleDateString()}`);
};
return (
<View style={styles.container}>
{loading ? (
<ActivityIndicator size="large" color="#FF5733" />
) : (
<>
{/* Scheduled Maintenance Alerts */}
<View style={styles.scheduledContainer}>
<Text style={styles.sectionTitle}>Upcoming Maintenance</Text>
{scheduledMaintenance.length > 0 ? (
<FlatList
data={scheduledMaintenance.filter(item => item.vehicleId === vehicleId)}
renderItem={({item}) => (
<View style={styles.alertCard}>
<Icon name="wrench" size={24} color="#FF5733" />
<View style={styles.alertInfo}>
<Text style={styles.alertTitle}>{item.type}</Text>
<Text style={styles.alertDate}>
Scheduled for: {new Date(item.scheduledDate).toLocaleDateString()}
</Text>
</View>
<TouchableOpacity
style={styles.completeButton}
onPress={() => {
// Complete maintenance and add to records
dispatch(addMaintenanceRecord({
vehicleId,
record: {
type: item.type,
notes: 'Completed scheduled maintenance',
timestamp: new Date().toISOString()
}
}));
dispatch(removeScheduledMaintenance(item.id));
}}
>
<Text style={styles.buttonText}>Complete</Text>
</TouchableOpacity>
</View>
)}
keyExtractor={item => item.id.toString()}
ListEmptyComponent={<Text>No upcoming maintenance scheduled</Text>}
/>
) : (
<Text>No upcoming maintenance scheduled</Text>
)}
</View>
{/* Quick Schedule Buttons */}
<View style={styles.quickSchedule}>
<Text style={styles.subTitle}>Quick Schedule:</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={styles.scheduleButton}
onPress={() => scheduleNextMaintenance('Oil Change', 90)}
>
<Text style={styles.buttonText}>Oil Change</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.scheduleButton}
onPress={() => scheduleNextMaintenance('Tire Rotation', 180)}
>
<Text style={styles.buttonText}>Tire Rotation</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.scheduleButton}
onPress={() => scheduleNextMaintenance('Full Service', 365)}
>
<Text style={styles.buttonText}>Full Service</Text>
</TouchableOpacity>
</View>
</View>
{/* Maintenance History */}
<View style={styles.historyContainer}>
<View style={styles.historyHeader}>
<Text style={styles.sectionTitle}>Maintenance History</Text>
<TouchableOpacity
style={styles.addButton}
onPress={() => setShowAddForm(!showAddForm)}
>
<Text style={styles.buttonText}>
{showAddForm ? 'Cancel' : 'Add Record'}
</Text>
</TouchableOpacity>
</View>
{showAddForm && (
<View style={styles.addForm}>
<Picker
selectedValue={maintenanceType}
onValueChange={setMaintenanceType}
style={styles.picker}
>
<Picker.Item label="Select type..." value="" />
<Picker.Item label="Oil Change" value="Oil Change" />
<Picker.Item label="Tire Rotation" value="Tire Rotation" />
<Picker.Item label="Brake Service" value="Brake Service" />
<Picker.Item label="Filter Replacement" value="Filter Replacement" />
<Picker.Item label="Other" value="Other" />
</Picker>
<TextInput
style={styles.notesInput}
placeholder="Maintenance notes..."
value={notes}
onChangeText={setNotes}
multiline
/>
<TouchableOpacity
style={styles.submitButton}
onPress={handleAddRecord}
>
<Text style={styles.buttonText}>Add Record</Text>
</TouchableOpacity>
</View>
)}
<FlatList
data={vehicleRecords}
renderItem={({item}) => (
<View style={styles.recordCard}>
<View style={styles.recordHeader}>
<Text style={styles.recordType}>{item.type}</Text>
<Text style={styles.recordDate}>
{new Date(item.timestamp).toLocaleDateString()}
</Text>
</View>
<Text style={styles.recordNotes}>{item.notes}</Text>
</View>
)}
keyExtractor={(item, index) => index.toString()}
ListEmptyComponent={<Text>No maintenance records found</Text>}
/>
</View>
</>
)}
</View>
);
};
- Analytics Dashboard
const analyticsSlice = createSlice({
name: 'analytics',
initialState: {
bookingTrends: [],
popularVehicles: [],
revenueData: {},
timeRange: 'month',
loading: false
},
reducers: {
setTimeRange: (state, action) => {
state.timeRange = action.payload;
}
},
extraReducers: (builder) => {
builder
.addCase(fetchAnalytics.pending, (state) => {
state.loading = true;
})
.addCase(fetchAnalytics.fulfilled, (state, action) => {
state.loading = false;
const { bookingTrends, popularVehicles, revenueData } = action.payload;
state.bookingTrends = bookingTrends;
state.popularVehicles = popularVehicles;
state.revenueData = revenueData;
});
}
});
frontend implemention
const AnalyticsDashboardScreen = () => {
const dispatch = useDispatch();
const { bookingTrends, popularVehicles, revenueData, timeRange, loading } = useSelector(state => state.analytics);
useEffect(() => {
dispatch(fetchAnalytics({ timeRange }));
}, [timeRange]);
const handleTimeRangeChange = (range) => {
dispatch(setTimeRange(range));
};
const renderBookingTrendChart = () => {
if (!bookingTrends || bookingTrends.length === 0) return null;
return (
<View style={styles.chartContainer}>
<Text style={styles.chartTitle}>Booking Trends</Text>
<LineChart
data={{
labels: bookingTrends.map(item => item.label),
datasets: [{
data: bookingTrends.map(item => item.count)
}]
}}
width={width - 40}
height={220}
chartConfig={{
backgroundColor: '#FFFFFF',
backgroundGradientFrom: '#FFFFFF',
backgroundGradientTo: '#FFFFFF',
decimalPlaces: 0,
color: (opacity = 1) => `rgba(66, 133, 244, ${opacity})`,
labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
}}
bezier
style={styles.chart}
/>
</View>
);
};
const renderPopularVehiclesChart = () => {
if (!popularVehicles || popularVehicles.length === 0) return null;
return (
<View style={styles.chartContainer}>
<Text style={styles.chartTitle}>Most Popular Vehicles</Text>
<BarChart
data={{
labels: popularVehicles.map(item => item.name.split(' ')[0]), // First word of name
datasets: [{
data: popularVehicles.map(item => item.bookingCount)
}]
}}
width={width - 40}
height={220}
chartConfig={{
backgroundColor: '#FFFFFF',
backgroundGradientFrom: '#FFFFFF',
backgroundGradientTo: '#FFFFFF',
decimalPlaces: 0,
color: (opacity = 1) => `rgba(255, 87, 51, ${opacity})`,
labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
}}
style={styles.chart}
/>
</View>
);
};
const renderRevenueCard = () => {
if (!revenueData) return null;
return (
<View style={styles.revenueCard}>
<Text style={styles.revenueTitle}>Revenue Summary</Text>
<View style={styles.metricsContainer}>
<View style={styles.metric}>
<Text style={styles.metricValue}>${revenueData.total.toFixed(2)}</Text>
<Text style={styles.metricLabel}>Total Revenue</Text>
</View>
<View style={styles.metric}>
<Text style={styles.metricValue}>${revenueData.average.toFixed(2)}</Text>
<Text style={styles.metricLabel}>Avg. per Booking</Text>
</View>
<View style={styles.metric}>
<Text style={[
styles.metricValue,
revenueData.percentChange >= 0 ? styles.positiveChange : styles.negativeChange
]}>
{revenueData.percentChange >= 0 ? '+' : ''}{revenueData.percentChange}%
</Text>
<Text style={styles.metricLabel}>vs. Previous</Text>
</View>
</View>
</View>
);
};
return (
<View style={styles.container}>
{/* Time Range Selector */}
<View style={styles.timeRangeSelector}>
{['week', 'month', 'quarter', 'year'].map(range => (
<TouchableOpacity
key={range}
style={[styles.rangeButton, timeRange === range && styles.activeRangeButton]}
onPress={() => handleTimeRangeChange(range)}
>
<Text style={[
styles.rangeButtonText,
timeRange === range && styles.activeRangeButtonText
]}>
{range.charAt(0).toUpperCase() + range.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
{loading ? (
<ActivityIndicator size="large" color="#FF5733" style={styles.loader} />
) : (
<ScrollView contentContainerStyle={styles.scrollContent}>
{renderRevenueCard()}
{renderBookingTrendChart()}
{renderPopularVehiclesChart()}
{/* Top Performing Locations */}
<View style={styles.locationsContainer}>
<Text style={styles.sectionTitle}>Top Performing Locations</Text>
{revenueData?.topLocations?.map((location, index) => (
<View key={index} style={styles.locationItem}>
<Text style={styles.locationName}>{location.name}</Text>
<View style={styles.locationStats}>
<Text style={styles.bookingCount}>{location.bookings} bookings</Text>
<Text style={styles.locationRevenue}>${location.revenue.toFixed(2)}</Text>
</View>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${(location.revenue / revenueData.topLocations[0].revenue) * 100}%` }
]}
/>
</View>
</View>
))}
</View>
</ScrollView>
)}
</View>
);
};
Top comments (0)