The Incidents App provides near real-time data updates to ensure all users see the latest incident information without manual refreshing.
Overview
While the app doesn’t currently implement WebSocket-based real-time subscriptions, it uses several strategies to provide timely data updates:
Focus-Based Refresh
Automatically reload data when screens come into focus
Pull-to-Refresh
Manual refresh trigger for users to update data
Action-Based Reload
Reload data after mutations and state changes
Focus-Based Updates
The app uses Expo Router’s useFocusEffect to reload data when screens become active:
Employee Task List
import { useFocusEffect } from 'expo-router'
import { useCallback } from 'react'
export const EmpleadoMyTasks = () => {
const [incidents, setIncidents] = useState<Incident[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadIncidents()
}, [])
useFocusEffect(
useCallback(() => {
loadIncidents()
}, []),
)
const loadIncidents = async () => {
// Fetch latest data from Supabase
const { data, error } = await supabase
.from("incidents")
.select("id, title, description, priority, status, created_at, areas(name)")
.eq("assigned_to", user.id)
.order("created_at", { ascending: false })
setIncidents(data || [])
}
}
useFocusEffect runs every time the screen comes into focus, ensuring users always see fresh data when navigating back to a screen.
General Inbox
The same pattern is used for the general inbox:
EmpleadoBuzonIncidents.tsx
useFocusEffect(
useCallback(() => {
loadIncidents()
}, []),
)
Pull-to-Refresh
Users can manually trigger a data refresh by pulling down on lists:
EmpleadoBuzonIncidents.tsx
import { RefreshControl } from 'react-native'
export const EmpleadoBuzonIncidents = () => {
const [refreshing, setRefreshing] = useState(false)
const onRefresh = async () => {
setRefreshing(true)
await loadIncidents()
setRefreshing(false)
}
return (
<FlatList
data={incidents}
renderItem={renderIncident}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#2563EB"
colors={["#2563EB"]}
/>
}
/>
)
}
User Pulls Down
User initiates pull-to-refresh gesture on the list
Loading State
refreshing state is set to true, showing loading indicator
Data Fetch
loadIncidents() fetches fresh data from Supabase
UI Update
New data is displayed and loading indicator disappears
Action-Based Reloading
After user actions that modify data, the app reloads to show the updated state:
After Accepting a Task
const handleAcceptTask = async () => {
setActionLoading(true)
const { error } = await supabase
.from("incidents")
.update({
status: "recibida",
assigned_to: currentUserId,
})
.eq("id", id)
if (!error) {
Alert.alert("Éxito", "Has aceptado la tarea correctamente", [
{
text: "OK",
onPress: () => loadIncident(), // Reload to show updated state
},
])
}
setActionLoading(false)
}
After Status Update
const handleStatusClick = async () => {
if (incident?.status !== "recibida" || !isAssignedToMe) return
setActionLoading(true)
const { error } = await supabase
.from("incidents")
.update({
status: "en_progreso",
updated_at: new Date().toISOString(),
})
.eq("id", id)
if (!error) {
await loadIncident() // Reload to reflect new status
}
setActionLoading(false)
}
After Resolving an Incident
const handleResolveSuccess = () => {
Alert.alert("Éxito", "Has marcado la incidencia como resuelta", [
{
text: "OK",
onPress: () => {
loadIncident() // Reload to show resolution details
},
},
])
}
Loading States
The app uses skeleton screens to provide feedback during data loading:
const IncidentSkeleton = () => {
const pulseAnim = useRef(new Animated.Value(0)).current
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 0,
duration: 1000,
useNativeDriver: true,
}),
]),
).start()
}, [])
const opacity = pulseAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 0.7],
})
return (
<View style={cardStyle}>
<Animated.View style={[skeletonStyle, { opacity }]} />
{/* More skeleton elements */}
</View>
)
}
Skeleton screens improve perceived performance by showing a loading structure that matches the actual content layout.
Conditional Rendering
if (loading && !refreshing) {
return (
<FlatList
data={[1, 2, 3, 4]}
renderItem={() => <IncidentSkeleton />}
keyExtractor={(item) => `skeleton-${item}`}
/>
)
}
if (incidents.length === 0) {
return (
<EmptyState
title="Todo está tranquilo"
message="No hay nuevas incidencias reportadas en el buzón."
/>
)
}
return (
<FlatList
data={incidents}
renderItem={renderIncident}
refreshControl={<RefreshControl ... />}
/>
)
Optimistic Updates
While the app doesn’t implement full optimistic updates, it uses local state to provide immediate feedback:
const [actionLoading, setActionLoading] = useState(false)
const handleAcceptTask = async () => {
setActionLoading(true) // Immediate UI feedback
try {
const { error } = await supabase.from("incidents").update(...)
if (!error) {
// Update was successful, reload fresh data
await loadIncident()
}
} finally {
setActionLoading(false)
}
}
Future: Supabase Realtime
For true real-time updates, the app can be enhanced with Supabase Realtime subscriptions:
import { useEffect } from 'react'
import { supabase } from '@/src/services/supabase'
export const useRealtimeIncidents = (areaId: string) => {
useEffect(() => {
const channel = supabase
.channel('incidents')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'incidents',
filter: `area_id=eq.${areaId}`,
},
(payload) => {
console.log('Change received!', payload)
// Update local state based on payload
if (payload.eventType === 'INSERT') {
// New incident added
} else if (payload.eventType === 'UPDATE') {
// Incident updated
} else if (payload.eventType === 'DELETE') {
// Incident deleted
}
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [areaId])
}
Benefits of Realtime Subscriptions
- Instant updates without polling
- Reduced server load
- Better user experience
- No need for manual refresh
- Real-time collaboration
Data Consistency
Race Condition Prevention
When accepting tasks, the app checks current status before updating:
const { data: currentIncident, error: fetchError } = await supabase
.from("incidents")
.select("status")
.eq("id", id)
.single()
if (currentIncident.status !== "pendiente") {
Alert.alert(
"Tarea no disponible",
"Esta tarea ya fue aceptada por otro empleado del área."
)
return
}
const { error: updateError } = await supabase
.from("incidents")
.update({
status: "recibida",
assigned_to: currentUserId,
})
.eq("id", id)
This check-then-update pattern prevents two employees from accepting the same task simultaneously.
Query Optimization
Selective Field Fetching
Only fetch fields needed for the current view:
// List view - minimal fields
const { data } = await supabase
.from("incidents")
.select("id, title, priority, status, created_at, areas(name)")
.eq("assigned_to", user.id)
// Detail view - all fields with relations
const { data } = await supabase
.from("incidents")
.select(`
*,
areas(name),
rooms(room_code),
incident_resolutions(description, created_at, resolved_by),
incident_evidence(image_url)
`)
.eq("id", id)
.single()
Ordering and Pagination
const { data } = await supabase
.from("incidents")
.select("*")
.eq("area_id", areaId)
.order("created_at", { ascending: false })
.limit(50) // Pagination
.range(0, 49)
Update Patterns
Full Reload
Reload entire dataset after mutations Single Item Update
Update specific item in local statesetIncident(updatedIncident)
Incremental Update
Add new item to existing listsetIncidents([newIncident, ...incidents])
Filter Update
Remove items that no longer match criteriasetIncidents(incidents.filter(i => i.id !== removedId))
1. Debounce Updates
Prevent excessive API calls during rapid navigation:
const debouncedLoad = useCallback(
debounce(() => loadIncidents(), 300),
[]
)
2. Cache Data
Consider using a state management library to cache data:
// Using React Query (future enhancement)
import { useQuery } from '@tanstack/react-query'
const { data, refetch } = useQuery({
queryKey: ['incidents', user.id],
queryFn: loadIncidents,
staleTime: 30000, // Consider data fresh for 30 seconds
})
3. Optimistic UI Updates
Update UI immediately, then sync with server:
const updateStatus = async (incidentId: string, newStatus: string) => {
// Optimistic update
setIncidents(incidents.map(i =>
i.id === incidentId ? { ...i, status: newStatus } : i
))
// Server sync
const { error } = await supabase
.from('incidents')
.update({ status: newStatus })
.eq('id', incidentId)
if (error) {
// Rollback on error
loadIncidents()
}
}
Incident Tracking
See how updates affect incident tracking
Push Notifications
Get notified about updates
Database
Learn about data structure
Performance
Optimize app performance