SetRow Component
The SetRow component is a specialized input row used for logging individual sets during workout tracking. It displays set numbers, target values, provides inputs for weight and reps, and includes an animated completion button with haptic feedback.
Import
import { SetRow } from "@/components/SetRow" ;
import { LoggedSet } from "@/types/workout" ;
Basic Usage
Simple Set Row
Multiple Sets
Bodyweight Exercise
AMRAP Set
import { SetRow } from "@/components/SetRow" ;
import { useState } from "react" ;
function ExerciseDetail () {
const [ loggedSet , setLoggedSet ] = useState < LoggedSet >({
setNumber: 1 ,
weight: "" ,
reps: "" ,
completed: false
});
return (
< SetRow
setNumber = { 1 }
targetWeight = { 185 }
targetReps = { 5 }
loggedSet = { loggedSet }
onToggleComplete = { () => {
setLoggedSet ({ ... loggedSet , completed: ! loggedSet . completed });
} }
onUpdateWeight = { ( weight ) => {
setLoggedSet ({ ... loggedSet , weight });
} }
onUpdateReps = { ( reps ) => {
setLoggedSet ({ ... loggedSet , reps });
} }
/>
);
}
Props
The set number to display (e.g., 1, 2, 3)
Target weight for this set. Can be a number (e.g., 185) or “BW” for bodyweight exercises
Target reps for this set. Can be a number (e.g., 5) or “Max” for AMRAP sets
The logged data for this set:
setNumber: number - Set number
weight: number | string - Logged weight value
reps: number | string - Logged reps value
completed: boolean - Whether the set is marked complete
Callback invoked when the completion button is pressed
onUpdateWeight
(weight: string) => void
required
Callback invoked when the weight input changes
onUpdateReps
(reps: string) => void
required
Callback invoked when the reps input changes
LoggedSet Type
export interface LoggedSet {
setNumber : number ;
weight : number | string ;
reps : number | string ;
completed : boolean ;
}
Visual Breakdown
The SetRow has four main sections in a horizontal layout:
┌──────────┬────────────┬────────────┬────────┐
│ Set 1 │ Weight │ Reps │ ✓ │
│ │ [185] │ [5] │ │
│ │ 185 │ 5 │ │
└──────────┴────────────┴────────────┴────────┘
50px flex:1 flex:1 40px
1. Set Number Label
Fixed width (50px)
Displays “Set ”
Secondary text color
Target weight label above input
TextInput with numeric keyboard
Placeholder shows target value
Flex: 1 (equal width)
Target reps label above input
TextInput with numeric keyboard
Placeholder shows target value or “Max”
Flex: 1 (equal width)
Fixed size (40x40)
Animated press effect
Circle icon when incomplete
Check icon when complete
Real-World Example
From WorkoutScreen.tsx:407-420:
{ 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 ) }
/>
))}
State Management Pattern
The SetRow is a controlled component. Here’s the recommended pattern:
function ExerciseDetail () {
const [ loggedWorkout , setLoggedWorkout ] = useState < LoggedWorkout >({
exercises: [
{
tier: "T1" ,
exercise: "Bench Press" ,
sets: [
{ setNumber: 1 , weight: "" , reps: "" , completed: false },
{ setNumber: 2 , weight: "" , reps: "" , completed: false },
{ setNumber: 3 , weight: "" , reps: "" , completed: false },
]
}
]
});
const handleToggleSetComplete = async ( exerciseIndex : number , setIndex : number ) => {
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 ,
};
setLoggedWorkout ( updated );
await saveLoggedWorkout ( updated );
};
const handleUpdateWeight = async (
exerciseIndex : number ,
setIndex : number ,
weight : string
) => {
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 ],
weight ,
};
setLoggedWorkout ( updated );
await saveLoggedWorkout ( updated );
};
// Similar for handleUpdateReps...
}
Completion States
The SetRow has two visual states based on loggedSet.completed:
Background : theme.backgroundSecondaryButton :
Background: theme.backgroundDefault
Border: theme.border
Icon: Circle outline (circle icon)
Icon color: theme.textSecondary
< SetRow
loggedSet = { { setNumber: 1 , weight: "" , reps: "" , completed: false } }
// other props...
/>
Background : theme.success with 15% opacity (${theme.success}15)Button :
Background: theme.success
Border: theme.success
Icon: Checkmark (check icon)
Icon color: #FFFFFF
< SetRow
loggedSet = { { setNumber: 1 , weight: "185" , reps: "5" , completed: true } }
// other props...
/>
Animation Behavior
When the completion button is pressed:
const handleToggle = () => {
if ( Platform . OS !== "web" ) {
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium );
}
scale . value = withSequence (
withSpring ( 0.95 , { damping: 15 }),
withSpring ( 1 , { damping: 15 })
);
onToggleComplete ();
};
Haptic Feedback : Medium impact on iOS/Android
Scale Down : Animates to 0.95 with spring
Scale Up : Springs back to 1.0
Callback : Invokes onToggleComplete
< TextInput
style = { [ styles . input , { ... }] }
value = { loggedSet ?. weight ?. toString () ?? "" }
onChangeText = { onUpdateWeight }
placeholder = { targetWeight === "BW" ? "BW" : String ( targetWeight ) }
placeholderTextColor = { theme . textSecondary }
keyboardType = "numeric"
testID = { `set- ${ setNumber } -weight-input` }
/>
Value : Controlled by loggedSet.weight
Placeholder : Shows target value
Keyboard : Numeric for easy number entry
Test ID : For automated testing
< TextInput
style = { [ styles . input , { ... }] }
value = { loggedSet ?. reps ?. toString () ?? "" }
onChangeText = { onUpdateReps }
placeholder = { targetReps === "Max" ? "Max" : String ( targetReps ) }
placeholderTextColor = { theme . textSecondary }
keyboardType = "numeric"
testID = { `set- ${ setNumber } -reps-input` }
/>
Value : Controlled by loggedSet.reps
Placeholder : Shows “Max” for AMRAP or target number
Keyboard : Numeric input
Test ID : For automated testing
Styling
Default Styles
const styles = StyleSheet . create ({
container: {
flexDirection: "row" ,
alignItems: "center" ,
padding: Spacing . md ,
borderRadius: BorderRadius . xs ,
marginBottom: Spacing . sm ,
gap: Spacing . md ,
},
setNumber: {
width: 50 ,
},
setLabel: {
fontSize: 14 ,
fontWeight: "500" ,
},
inputGroup: {
flex: 1 ,
},
targetLabel: {
fontSize: 11 ,
marginBottom: 2 ,
},
input: {
height: 36 ,
borderRadius: BorderRadius . xs ,
borderWidth: 1 ,
paddingHorizontal: Spacing . sm ,
fontSize: 14 ,
fontWeight: "500" ,
},
checkButton: {
width: 40 ,
height: 40 ,
borderRadius: 20 ,
alignItems: "center" ,
justifyContent: "center" ,
borderWidth: 2 ,
},
});
Theme Colors
Container Background (incomplete) : theme.backgroundSecondary
Container Background (complete) : ${theme.success}15 (15% opacity)
Input Background : theme.backgroundDefault
Input Border : theme.border
Input Text : theme.text
Labels : theme.textSecondary
Placeholder : theme.textSecondary
Test IDs
The SetRow includes test IDs for automated testing:
testID = { `set- ${ setNumber } -weight-input` }
testID = { `set- ${ setNumber } -reps-input` }
testID = { `set- ${ setNumber } -complete-button` }
Example test IDs:
set-1-weight-input
set-1-reps-input
set-1-complete-button
Best Practices
Persist data on every change
Save logged data immediately when inputs change: const handleUpdateWeight = async ( index : number , weight : string ) => {
// Update state
const updated = { ... loggedWorkout };
updated . exercises [ exerciseIndex ]. sets [ index ]. weight = weight ;
setLoggedWorkout ( updated );
// Persist immediately
await saveLoggedWorkout ( updated );
};
Handle special values gracefully
Support both numbers and special strings: // Bodyweight
targetWeight = "BW"
loggedSet = {{ weight : "BW" , ... }}
// AMRAP
targetReps = "Max"
loggedSet = {{ reps : "12" , ... }} // User enters actual reps achieved
The component includes haptic feedback on completion toggle. Ensure expo-haptics is installed: npx expo install expo-haptics
Accessibility
Touch Targets : Inputs and button meet minimum 40px size
Keyboard Type : Numeric keyboard for easier number entry
Visual Feedback : Clear completion state with color and icon
Haptic Feedback : Tactile confirmation on completion (iOS/Android)
Test IDs : Support for automated testing and screen readers
Haptic Feedback
Haptics are disabled on web:
if ( Platform . OS !== "web" ) {
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium );
}
Keyboard Behavior
Numeric keyboard appears automatically on mobile when inputs are focused.
Dependencies
ThemedText - For labels
useTheme - For theme integration
react-native-reanimated - For animations
expo-haptics - For haptic feedback
@expo/vector-icons - For check/circle icons
@/types/workout - For TypeScript interfaces
Source Code
Location: client/components/SetRow.tsx:1-181
The SetRow component is designed to be used in lists of sets for exercise logging, providing a consistent and intuitive interface for workout tracking.