Skip to main content
The GoalsScreen component allows users to set their goal weights (1RM or training maxes) for all exercises in the Rippler program. These values are used to calculate target weights throughout the 12-week training cycle.

Overview

The GoalsScreen provides a categorized interface for entering goal weights across three tiers:
  • T1 (Main Lifts): The four primary compound movements with 1RM goals
  • T2 (Secondary Lifts): Supporting compound movements
  • T3 (Accessories): Isolation and accessory work
Location: ~/workspace/source/client/screens/GoalsScreen.tsx

Key Features

Tiered Exercise Organization

Exercises grouped by tier (T1/T2/T3) for clear hierarchy

Persistent Storage

Goal weights saved to AsyncStorage and persist across sessions

Change Tracking

Visual feedback when goals are modified but not saved

Bodyweight Handling

Special handling for bodyweight exercises like pull-ups

State Management

const [goals, setGoals] = useState<Record<string, number>>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);

Loading Goals

The component loads goals on mount and when focused:
const loadGoals = useCallback(async () => {
  setLoading(true);
  try {
    const savedGoals = await getGoalWeights();
    const defaultGoals = getDefaultGoals();
    const mergedGoals = { ...defaultGoals, ...savedGoals };
    setGoals(mergedGoals);
    setHasChanges(false);
  } catch (error) {
    console.error("Error loading goals:", error);
  } finally {
    setLoading(false);
  }
}, []);

useFocusEffect(
  useCallback(() => {
    loadGoals();
  }, [loadGoals])
);
The component merges saved goals with defaults to ensure all exercises have values, even if the user hasn’t set them yet.

User Interactions

const handleGoalChange = (exercise: string, value: string) => {
  const numValue = parseFloat(value) || 0;
  setGoals((prev) => ({ ...prev, [exercise]: numValue }));
  setHasChanges(true);
};
Input fields update the goals state immediately and mark hasChanges as true.

Exercise Input Rendering

const renderExerciseInput = (exercise: string, label?: string) => {
  const isBodyweight = exercise === "Pull Ups";

  return (
    <View key={exercise} style={styles.inputRow}>
      <ThemedText style={styles.exerciseLabel}>
        {label || exercise}
      </ThemedText>
      {isBodyweight ? (
        <View style={[styles.inputDisabled, { backgroundColor: theme.backgroundSecondary }]}>
          <ThemedText style={styles.bodyweightText}>BW</ThemedText>
        </View>
      ) : (
        <TextInput
          style={[styles.input, { /* theme styles */ }]}
          value={goals[exercise]?.toString() || ""}
          onChangeText={(text) => handleGoalChange(exercise, text)}
          keyboardType="decimal-pad"
          placeholder="0"
        />
      )}
      <ThemedText style={styles.unitLabel}>lbs</ThemedText>
    </View>
  );
};
Bodyweight exercises like pull-ups display “BW” instead of an input field, since the weight calculation is based on the user’s bodyweight.

UI Structure

<ScrollView>
  <ThemedText style={styles.description}>
    Enter your goal or training max for each exercise. Targets throughout
    the 12-week program will be calculated based on these values.
  </ThemedText>

  {/* T1 Main Lifts */}
  <Card>
    <ThemedText style={styles.sectionTitle}>T1 - Main Lifts (1RM Goals)</ThemedText>
    {mainLifts.map((exercise) => renderExerciseInput(exercise))}
  </Card>

  {/* T2 Secondary Lifts */}
  <Card>
    <ThemedText style={styles.sectionTitle}>T2 - Secondary Lifts</ThemedText>
    {t2Lifts.map((exercise) => renderExerciseInput(exercise))}
  </Card>

  {/* T3 Accessories */}
  <Card>
    <ThemedText style={styles.sectionTitle}>T3 - Accessories</ThemedText>
    {t3Lifts.map((exercise) => renderExerciseInput(exercise))}
  </Card>

  <View style={styles.buttonRow}>
    <Pressable onPress={handleReset}>Reset Defaults</Pressable>
    <Pressable 
      onPress={handleSave} 
      disabled={saving || !hasChanges}
      style={{ backgroundColor: hasChanges ? theme.primary : theme.border }}
    >
      {saving ? <ActivityIndicator /> : "Save Goals"}
    </Pressable>
  </View>
</ScrollView>

Data Flow

Integration with Other Components

The ProgramScreen loads goal weights to display the main lift target weight for each workout day:
const [goalWeights, setGoalWeights] = useState<Record<string, number>>({});

const loadData = async () => {
  const goals = await getGoalWeights();
  setGoalWeights(goals);
};

const getMainLiftWeight = (workout: WorkoutDay): number | string | undefined => {
  const mergedGoals = { ...getDefaultGoals(), ...goalWeights };
  const calculatedWeight = calculateTargetWeight(
    mainLift.exercise,
    workout.week,
    workout.day,
    mainLiftIndex,
    mergedGoals
  );
  return calculatedWeight;
};
The WorkoutScreen uses goal weights to calculate target weights for all exercises:
const getEffectiveTarget = useCallback((target: TargetExercise, index: number) => {
  const mergedGoals = { ...getDefaultGoals(), ...goalWeights };
  const calculatedWeight = calculateTargetWeight(
    target.exercise,
    week,
    day,
    index,
    mergedGoals
  );
  return calculatedWeight;
}, [goalWeights, week, day]);
Goal weights are persisted using AsyncStorage:
// lib/storage.ts
export const getGoalWeights = async (): Promise<Record<string, number>> => {
  const data = await AsyncStorage.getItem(GOAL_WEIGHTS_KEY);
  return data ? JSON.parse(data) : {};
};

export const saveGoalWeights = async (goals: Record<string, number>): Promise<void> => {
  await AsyncStorage.setItem(GOAL_WEIGHTS_KEY, JSON.stringify(goals));
};

Layout & Styling

The screen uses navigation hooks for proper spacing:
const headerHeight = useHeaderHeight();
const tabBarHeight = useBottomTabBarHeight();

<ScrollView
  contentContainerStyle={[
    styles.scrollContent,
    {
      paddingTop: headerHeight + Spacing.md,
      paddingBottom: tabBarHeight + Spacing.xl,
    },
  ]}
>
The save button changes color based on hasChanges state, providing clear visual feedback when modifications are pending.

Best Practices

1

Load on Focus

Use useFocusEffect to reload goals when the screen gains focus, ensuring data stays fresh
2

Merge with Defaults

Always merge saved goals with defaults to handle new exercises or missing data
3

Track Changes

Maintain a hasChanges flag to enable/disable the save button and prevent unnecessary saves
4

Handle Bodyweight

Special case bodyweight exercises to prevent user confusion
5

Provide Feedback

Use haptic feedback on mobile to confirm successful saves

Build docs developers (and LLMs) love