The WorkoutScreen component is where users perform and log their workouts. It displays exercises for a specific week and day, allows input of weight and reps for each set, tracks completion, and supports editing target weights.
Overview
This is the most complex screen in the app, handling:
Real-time set logging and completion tracking
Dynamic target weight calculations based on goal weights
Exercise-level expand/collapse for better UX
Target override functionality (modify reps/sets/weight for specific exercises)
Progress visualization
Haptic feedback for user actions
Location: ~/workspace/source/client/screens/WorkoutScreen.tsx
Key Features
Real-time Logging Log weight and reps for each set with immediate save to storage
Target Overrides Modify target weight, reps, or sets for any exercise in the workout
Progress Tracking Visual progress bar and set completion indicators
Expandable Exercises Collapse completed exercises to focus on current work
Route Parameters
type WorkoutRouteProp = RouteProp < RootStackParamList , "Workout" >;
const route = useRoute < WorkoutRouteProp >();
const { week , day } = route . params ;
// Example: { week: 1, day: "Upper A" }
The screen receives week and day parameters from navigation, used to load the correct workout template.
State Management
The screen maintains multiple pieces of state:
const [ loggedWorkout , setLoggedWorkout ] = useState < LoggedWorkout | null >( null );
const [ expandedExercise , setExpandedExercise ] = useState < number | null >( 0 );
const [ targetOverrides , setTargetOverrides ] = useState < TargetOverride []>([]);
const [ goalWeights , setGoalWeights ] = useState < Record < string , number >>({});
// Edit modal state
const [ editModalVisible , setEditModalVisible ] = useState ( false );
const [ editingExerciseIndex , setEditingExerciseIndex ] = useState < number | null >( null );
const [ editWeight , setEditWeight ] = useState ( "" );
const [ editReps , setEditReps ] = useState ( "" );
const [ editSets , setEditSets ] = useState ( "" );
Data Loading
const loadData = async () => {
setDataLoaded ( false );
const [ overrides , logged , goals ] = await Promise . all ([
getTargetOverridesForDay ( week , day ),
getLoggedWorkoutForDay ( week , day ),
getGoalWeights (),
]);
setTargetOverrides ( overrides );
setGoalWeights ( goals );
if ( logged ) {
setLoggedWorkout ( logged );
} else {
setLoggedWorkout ( null );
}
setDataLoaded ( true );
};
Initial Load
Focus Effect
Workout Initialization
useEffect (() => {
loadData ();
}, [ week , day ]);
useFocusEffect (
useCallback (() => {
loadData ();
}, [ week , day ])
);
useEffect (() => {
if ( dataLoaded && ! loggedWorkout && targetWorkout ) {
setLoggedWorkout ( initializeLoggedWorkout ());
}
}, [ dataLoaded , targetWorkout ]);
Creates a new logged workout if none exists for this week/day.
Target Weight Calculation
The getEffectiveTarget function combines multiple data sources:
const getEffectiveTarget = useCallback (( target : TargetExercise , index : number ) : EffectiveTarget => {
const override = targetOverrides . find (( o ) => o . exerciseIndex === index );
// Calculate base weight from goal weights
let baseWeight = target . weight ;
const mergedGoals = { ... getDefaultGoals (), ... goalWeights };
const calculatedWeight = calculateTargetWeight (
target . exercise ,
week ,
day ,
index ,
mergedGoals
);
if ( calculatedWeight !== null ) {
baseWeight = calculatedWeight ;
}
// Apply overrides if they exist
if ( override ) {
return {
... target ,
weight: override . weight !== undefined ? override . weight : baseWeight ,
reps: override . reps !== undefined ? override . reps : target . reps ,
sets: override . sets !== undefined ? override . sets : target . sets ,
isModified: true ,
};
}
return { ... target , weight: baseWeight , isModified: false };
}, [ targetOverrides , goalWeights , week , day ]);
This function creates a three-tier priority system: overrides > calculated weights > default weights
Workout Initialization
const initializeLoggedWorkout = useCallback (() : LoggedWorkout => {
return {
id: ` ${ week } - ${ day } ` ,
week ,
day ,
dateLogged: new Date (). toISOString (),
exercises: targetWorkout . exercises . map (( ex , index ) => {
const effective = getEffectiveTarget ( ex , index );
const totalSets = typeof effective . sets === "number"
? effective . sets
: parseInt ( String ( effective . sets ). replace ( "+" , "" ), 10 );
return {
tier: ex . tier ,
exercise: ex . exercise ,
sets: Array . from ({ length: totalSets }, ( _ , i ) => ({
setNumber: i + 1 ,
weight: "" ,
reps: "" ,
completed: false ,
})),
};
}),
completed: false ,
};
}, [ week , day , targetWorkout , getEffectiveTarget ]);
Set Management
Toggle Completion
Update Weight
Update Reps
const handleToggleSetComplete = async ( exerciseIndex : number , setIndex : number ) => {
if ( ! loggedWorkout ) return ;
const updated = { ... loggedWorkout };
updated . exercises = [ ... updated . exercises ];
updated . exercises [ exerciseIndex ] = { ... updated . exercises [ exerciseIndex ] };
updated . exercises [ exerciseIndex ]. sets = [ ... updated . exercises [ exerciseIndex ]. sets ];
updated . exercises [ exerciseIndex ]. sets [ setIndex ] = {
... updated . exercises [ exerciseIndex ]. sets [ setIndex ],
completed: ! updated . exercises [ exerciseIndex ]. sets [ setIndex ]. completed ,
};
// Check if all sets are completed
const allCompleted = updated . exercises . every (( ex ) =>
ex . sets . every (( s ) => s . completed )
);
updated . completed = allCompleted ;
setLoggedWorkout ( updated );
await saveLoggedWorkout ( updated );
};
const handleUpdateWeight = async (
exerciseIndex : number ,
setIndex : number ,
weight : string
) => {
if ( ! loggedWorkout ) return ;
const updated = { ... loggedWorkout };
updated . exercises [ exerciseIndex ]. sets [ setIndex ]. weight = weight ;
setLoggedWorkout ( updated );
await saveLoggedWorkout ( updated );
};
const handleUpdateReps = async (
exerciseIndex : number ,
setIndex : number ,
reps : string
) => {
if ( ! loggedWorkout ) return ;
const updated = { ... loggedWorkout };
updated . exercises [ exerciseIndex ]. sets [ setIndex ]. reps = reps ;
setLoggedWorkout ( updated );
await saveLoggedWorkout ( updated );
};
All updates are immediately saved to AsyncStorage to prevent data loss if the app crashes or is closed.
Target Override System
Users can modify targets for individual exercises:
const openEditModal = ( index : number ) => {
if ( ! targetWorkout ) return ;
const effective = getEffectiveTarget ( targetWorkout . exercises [ index ], index );
setEditingExerciseIndex ( index );
setEditWeight ( String ( effective . weight ));
setEditReps ( String ( effective . reps ));
setEditSets ( String ( effective . sets ). replace ( "+" , "" ));
setEditModalVisible ( true );
};
const handleSaveTarget = async () => {
if ( editingExerciseIndex === null ) return ;
const override : TargetOverride = {
week ,
day ,
exerciseIndex: editingExerciseIndex ,
weight: editWeight ,
reps: editReps ,
sets: editSets ,
};
await saveTargetOverride ( override );
// Update local state
const updatedOverrides = [ ... targetOverrides ];
const existingIndex = updatedOverrides . findIndex (
( o ) => o . exerciseIndex === editingExerciseIndex
);
if ( existingIndex !== - 1 ) {
updatedOverrides [ existingIndex ] = override ;
} else {
updatedOverrides . push ( override );
}
setTargetOverrides ( updatedOverrides );
// Adjust logged workout if set count changed
if ( loggedWorkout && targetWorkout ) {
const totalSets = parseInt ( String ( editSets ). replace ( "+" , "" ), 10 );
const currentSets = loggedWorkout . exercises [ editingExerciseIndex ]?. sets . length || 0 ;
if ( totalSets !== currentSets ) {
const updated = { ... loggedWorkout };
updated . exercises [ editingExerciseIndex ]. sets = Array . from ({ length: totalSets }, ( _ , i ) => {
const existingSet = loggedWorkout . exercises [ editingExerciseIndex ]?. sets [ i ];
return existingSet || {
setNumber: i + 1 ,
weight: "" ,
reps: "" ,
completed: false ,
};
});
setLoggedWorkout ( updated );
await saveLoggedWorkout ( updated );
}
}
setEditModalVisible ( false );
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success );
};
When set count changes, the system preserves existing logged data and adds/removes sets as needed.
Exercise Rendering
const renderExercise = (
target : TargetExercise ,
logged : LoggedExercise | undefined ,
index : number
) => {
const effective = getEffectiveTarget ( target , index );
const isExpanded = expandedExercise === index ;
const completedSets = logged ?. sets . filter (( s ) => s . completed ). length || 0 ;
const totalSets = logged ?. sets . length || 0 ;
return (
< View style = {styles. exerciseCard } >
{ /* Header: Exercise name, tier badge, progress */ }
< Pressable
onPress = {() => setExpandedExercise ( isExpanded ? null : index )}
style = {styles. exerciseHeader }
>
< View style = {styles. exerciseInfo } >
< TierBadge tier = {target. tier } />
< ThemedText type = "h4" > {target. exercise } </ ThemedText >
</ View >
< View style = {styles. exerciseRight } >
< View style = {styles. progressBadge } >
< ThemedText >{ completedSets } / { totalSets } </ ThemedText >
</ View >
< Feather name = {isExpanded ? "chevron-up" : "chevron-down" } />
</ View >
</ Pressable >
{ /* Target info with edit button */ }
< Pressable style = {styles. targetInfo } onPress = {() => openEditModal ( index )} >
< View style = {styles. targetRow } >
< ThemedText >
Target : { effective . weight } lbs x {effective. reps } reps x { effective . sets } sets
</ ThemedText >
< Feather name = "edit-2" color = {effective.isModified ? theme. primary : theme . textSecondary } />
</ View >
{ effective . isModified && (
< ThemedText style = {{ color : theme . primary }} > Modified </ ThemedText >
)}
</ Pressable >
{ /* Expanded: Set rows */ }
{ isExpanded && logged && (
< View style = {styles. setsContainer } >
{ logged . sets . map (( set , setIndex ) => (
< SetRow
key = { setIndex }
setNumber = {set. setNumber }
targetWeight = {effective. weight }
targetReps = {effective. reps }
loggedSet = { set }
onToggleComplete = {() => handleToggleSetComplete ( index , setIndex )}
onUpdateWeight = {(weight) => handleUpdateWeight ( index , setIndex , weight )}
onUpdateReps = {(reps) => handleUpdateReps ( index , setIndex , reps )}
/>
))}
</ View >
)}
</ View >
);
};
Progress Visualization
const totalSets = loggedWorkout ?. exercises . reduce (
( acc , ex ) => acc + ex . sets . length ,
0
) || 0 ;
const completedSets = loggedWorkout ?. exercises . reduce (
( acc , ex ) => acc + ex . sets . filter (( s ) => s . completed ). length ,
0
) || 0 ;
< View style = {styles. progressCard } >
< ThemedText > Overall Progress </ ThemedText >
< View style = {styles. progressBar } >
< View
style = { [
styles.progressFill,
{
backgroundColor: theme.success,
width: totalSets > 0 ? ` ${ ( completedSets / totalSets ) * 100 } %` : "0%" ,
},
]}
/>
</View>
<ThemedText type="h4">
{completedSets} / {totalSets} sets completed
</ThemedText>
</View>
Complete Workout
const handleCompleteWorkout = async () => {
if ( ! loggedWorkout ) return ;
if ( Platform . OS !== "web" ) {
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success );
}
const updated = { ... loggedWorkout , completed: true };
updated . exercises = updated . exercises . map (( ex ) => ({
... ex ,
sets: ex . sets . map (( s ) => ({ ... s , completed: true })),
}));
setLoggedWorkout ( updated );
await saveLoggedWorkout ( updated );
navigation . goBack ();
};
Completing a workout marks all sets as completed and navigates back to ProgramScreen.
Edit Target Modal
< Modal
visible = { editModalVisible }
animationType = "slide"
transparent
onRequestClose = { () => setEditModalVisible ( false ) }
>
< KeyboardAvoidingView
behavior = { Platform . OS === "ios" ? "padding" : "height" }
style = { styles . modalOverlay }
>
< View style = { styles . modalContent } >
< View style = { styles . modalHeader } >
< ThemedText type = "h3" > Edit Target </ ThemedText >
< Pressable onPress = { () => setEditModalVisible ( false ) } >
< Feather name = "x" size = { 24 } />
</ Pressable >
</ View >
< ThemedText style = { styles . modalSubtitle } >
{ editingExerciseName }
</ ThemedText >
< View style = { styles . inputGroup } >
< ThemedText > Weight (lbs or BW) </ ThemedText >
< TextInput
value = { editWeight }
onChangeText = { setEditWeight }
placeholder = "e.g., 135 or BW"
/>
</ View >
< View style = { styles . inputGroup } >
< ThemedText > Reps (number or Max) </ ThemedText >
< TextInput
value = { editReps }
onChangeText = { setEditReps }
placeholder = "e.g., 5 or Max"
/>
</ View >
< View style = { styles . inputGroup } >
< ThemedText > Sets </ ThemedText >
< TextInput
value = { editSets }
onChangeText = { setEditSets }
keyboardType = "number-pad"
placeholder = "e.g., 3"
/>
</ View >
< View style = { styles . modalButtons } >
< Button variant = "secondary" onPress = { () => setEditModalVisible ( false ) } >
Cancel
</ Button >
< Button onPress = { handleSaveTarget } >
Save
</ Button >
</ View >
</ View >
</ KeyboardAvoidingView >
</ Modal >
Data Flow
Immediate State Updates : UI updates immediately, save happens asynchronously
Memoized Calculations : getEffectiveTarget is memoized with useCallback
Expandable Exercises : Only render set rows for expanded exercises
Parallel Data Loading : Load overrides, logged data, and goals simultaneously
Best Practices
Immutable Updates
Always create new objects when updating state to ensure React detects changes
Auto-save Everything
Save every change immediately to prevent data loss
Haptic Feedback
Provide tactile feedback for important actions (complete set, save override)
Preserve Data
When modifying set count, preserve existing logged data
Visual Feedback
Show “Modified” badge when targets have been overridden