Skip to main content

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

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

setNumber
number
required
The set number to display (e.g., 1, 2, 3)
targetWeight
number | string
required
Target weight for this set. Can be a number (e.g., 185) or “BW” for bodyweight exercises
targetReps
number | string
required
Target reps for this set. Can be a number (e.g., 5) or “Max” for AMRAP sets
loggedSet
LoggedSet
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
onToggleComplete
() => void
required
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

2. Weight Input Group

  • Target weight label above input
  • TextInput with numeric keyboard
  • Placeholder shows target value
  • Flex: 1 (equal width)

3. Reps Input Group

  • Target reps label above input
  • TextInput with numeric keyboard
  • Placeholder shows target value or “Max”
  • Flex: 1 (equal width)

4. Completion Button

  • 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...
/>

Animation Behavior

Completion Button Animation

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();
};
  1. Haptic Feedback: Medium impact on iOS/Android
  2. Scale Down: Animates to 0.95 with spring
  3. Scale Up: Springs back to 1.0
  4. Callback: Invokes onToggleComplete

Input Handling

Weight Input

<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

Reps Input

<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

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);
};
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
Always control inputs through state:
// Good - controlled
<SetRow
  loggedSet={sets[index]}
  onUpdateWeight={(weight) => updateSet(index, { weight })}
/>

// Bad - uncontrolled
<SetRow
  loggedSet={undefined}
  onUpdateWeight={() => {}}
/>

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

Platform Differences

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.

Build docs developers (and LLMs) love