Updating state array in react native

Viewed 41

I'm trying to keep track of a list of chosen exercises, and then render 'added' if the chosen exercise is in the list, or 'not added' if it's not in the list. I add the exercise to chosenExerciseArray when it's not already included, and remove it if it is. When I use console.log(chosenExerciseArray) it correctly displays the list, but if I use setChosenExercises(chosenExerciseArray) the state does not get updated correctly, and the chosenExerciseArray somehow stops logging the correct array as well.

I've also tried adding chosenExerciseArray as a useEffect dependency, but that causes an infinite loop. I'm really not sure what's going on here, but I believe it's an issue with my understanding of state.

EDIT: The TouchableOpacity is inside of a map, which may also be the issue, but I'm not sure how

<View style={styles.exerciseheadericongroup}>
   <TouchableOpacity
       onPress={() => {
         if(!chosenExerciseArray.includes(exercise.id)) {
            chosenExerciseArray.push(exercise.id);
         } else {
            for(var i=0; i<chosenExerciseArray.length; i++) {
               if(chosenExerciseArray[i] === exercise.id) {
                 chosenExerciseArray.splice(i, 1);
               }
             }
          }
          console.log(chosenExerciseArray);
        }}
    >
    {chosenExercises.includes(exercise.id) ? (
       <Text>added</Text>
    ) : (
       <Text>not added</Text>
    )}
  </TouchableOpacity>
</View>
1 Answers

As stated in the React documentation

state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props. [link]

I wish I could explain it better, but basically it means that manipulating chosenExerciseArray with .push and then executing setChosenExerciseArray(chosenExerciseArray) messes up the state handling. You need to make a copy of chosenExerciseArray and manipulate it instead, and then use the manipulated copy to set the new state. I have created two examples for you to look at:

Example 1 uses a different logic, where added is part of the exercise object itself, such that that the chosenExerciseArray is not necessary to keep track of whether the exercise is added or not. This is also a bit more efficient, because you do not need to check if an exercise is in the chosenExerciseArray or not.

import React, { useState } from "react";
import { Pressable, StyleSheet } from "react-native";

import { Text, View } from "../components/Themed";

interface Exercise {
  id: string;
  name: string;
  added: boolean;
}

const updateExerciseArray = (exercises: Exercise[], exercise: Exercise) => {
  // Shallow copy
  const updatedExercises = [...exercises];

  // Manipulate the copy
  return updatedExercises.map((updatedExercise) => {
    if (updatedExercise.id === exercise.id) {
      // Shallow copy of object in array where added is negated
      return { ...updatedExercise, added: !updatedExercise.added };
    }
    return updatedExercise;
  });
};

export default function Example1() {
  const [exercises, setExercises] = useState<Exercise[]>([
    { id: "1", name: "Pushups", added: false },
    { id: "2", name: "Situps", added: false },
    { id: "3", name: "Squats", added: false },
    { id: "4", name: "Pullups", added: false },
    { id: "5", name: "Leg Raises", added: false },
    { id: "6", name: "Plank", added: false },
    { id: "7", name: "Burpees", added: false },
    { id: "8", name: "Jumping Jacks", added: false },
    { id: "9", name: "Wall Sit", added: false },
  ]);

  return (
    <View style={styles.container}>
      {exercises.map((exercise) => {
        return (
          <Pressable
            key={exercise.id}
            onPress={() => {
              setExercises(updateExerciseArray(exercises, exercise));
            }}
            style={styles.row}
          >
            <Text style={styles.text}>{exercise.name}</Text>
            <Text style={styles.text}>
              {exercise.added ? "Added" : "Not added"}
            </Text>
          </Pressable>
        );
      })}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    paddingHorizontal: 50,
    flex: 1,
    backgroundColor: "#fff",
    justifyContent: "center",
  },
  row: {
    marginVertical: 10,
    flexDirection: "row",
    justifyContent: "space-between",
  },
  text: {
    fontSize: 20,
  },
});

Example 2 uses a solution that fits your current implementation.

import React, { useState } from "react";
import { Pressable, StyleSheet } from "react-native";

import { Text, View } from "../components/Themed";

interface Exercise {
  id: string;
  name: string;
}

const updateExerciseArray = (
  chosenExerciseArray: string[],
  exerciseToChange: Exercise
) => {
  // Shallow copy
  const updatedChosenExerciseArray = [...chosenExerciseArray];

  if (updatedChosenExerciseArray.includes(exerciseToChange.id)) {
    // Remove the exercise from the array
    return updatedChosenExerciseArray.filter(
      (exerciseId) => exerciseId !== exerciseToChange.id
    );
  } else {
    // Append the exercise to the array
    return [...updatedChosenExerciseArray, exerciseToChange.id];
  }
};

export default function Example2() {
  const [exercises] = useState<Exercise[]>([
    { id: "1", name: "Pushups" },
    { id: "2", name: "Situps" },
    { id: "3", name: "Squats" },
    { id: "4", name: "Pullups" },
    { id: "5", name: "Leg Raises" },
    { id: "6", name: "Plank" },
    { id: "7", name: "Burpees" },
    { id: "8", name: "Jumping Jacks" },
    { id: "9", name: "Wall Sit" },
  ]);
  const [chosenExerciseArray, setChosenExerciseArray] = useState<string[]>([
    "1",
    "2",
  ]);

  return (
    <View style={styles.container}>
      {exercises.map((exercise) => {
        return (
          <Pressable
            key={exercise.id}
            onPress={() => {
              setChosenExerciseArray(
                updateExerciseArray(chosenExerciseArray, exercise)
              );
            }}
            style={styles.row}
          >
            <Text style={styles.text}>{exercise.name}</Text>
            <Text style={styles.text}>
              {chosenExerciseArray.includes(exercise.id)
                ? "Added"
                : "Not added"}
            </Text>
          </Pressable>
        );
      })}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    paddingHorizontal: 50,
    flex: 1,
    backgroundColor: "#fff",
    justifyContent: "center",
  },
  row: {
    paddingVertical: 10,
    flexDirection: "row",
    justifyContent: "space-between",
  },
  text: {
    fontSize: 20,
  },
});

Related