Search code examples
reactjstsx

Why does deleting an item from state still require a page reload to reflect in UI?


I'm building a workout tracking app with React using typescript, and I’m having trouble with a delete button. I have all the saved workouts in a list and when I try to remove a workout from the list using a delete button, the UI doesn't immediately reflect the change. I have to reload the page for the workout to be deleted from the list. My understanding is that it'll re-render whenever a state changes and it "working" after reloading would mean that the state changed?

Here’s the relevant parts of my code: Calling the component from it's parent:

        <CalendarComponent savedWorkouts={savedWorkouts} />

Imports from other files I made:

import { Workout, Exercise } from '../utils/types';
import { assignWorkoutToDate, getWorkoutsForDate, removeWorkoutFromDate, calculateNextWeight, saveWorkouts, loadWorkouts} from '../utils/localStorage';

Declaring the states and functions:

const CalendarComponent: React.FC<CalendarProps> = ({ savedWorkouts }) => {
  const [selectedDate, setSelectedDate] = useState<Date>(new Date());
  const [selectedWorkouts, setSelectedWorkouts] = useState<Workout[]>([]);
  const [selectedDays, setSelectedDays] = useState<string[]>([]);
  const [assignedDays, setAssignedDays] = useState<Record<string, boolean>>({});
  const [workoutsForToday, setWorkoutsForToday] = useState<Workout[]>([]);
  const [modalWorkout, setModalWorkout] = useState<Workout | null>(null);
  const [editModal, setModalEdit] = useState(false);
  const [search, setSearch] = useState<string>('');
  
  useEffect(() => {
    // Fetch assigned workouts for the selected date
    const workouts = getWorkoutsForDate(selectedDate.toISOString().split('T')[0]);
    setWorkoutsForToday(workouts);
    // Reload assigned days from localStorage when the component mounts or when workouts change
    const storedAssignedDays = JSON.parse(localStorage.getItem('assignedDays') || '{}');
    setAssignedDays(storedAssignedDays);
  }, [selectedDate, savedWorkouts]);

//Other functions
  const filteredWorkouts = savedWorkouts.filter(workout =>
    workout.workoutName.toLowerCase().includes(search.toLowerCase())
  );
  const handleRemoveWorkoutFromList = (workout: Workout) => {
    // Update localStorage with the remaining workouts
    const workouts = loadWorkouts();
    const updatedWorkouts = workouts.filter(w => workout.workoutName !== w.workoutName);
    saveWorkouts(updatedWorkouts);

    savedWorkouts = updatedWorkouts;

    // Remove from selectedWorkouts and workoutsForToday state immediately
    setSelectedWorkouts(prevWorkouts => prevWorkouts.filter(w => w.workoutName !== workout.workoutName));
    setWorkoutsForToday(prevWorkouts => prevWorkouts.filter(w => w.workoutName !== workout.workoutName));
  };

This is in the return part:

      {/* Search and List of Workouts */}
      <div>
  <label>Search Workouts</label>
  <input
    type="text"
    value={search}
    onChange={(e) => setSearch(e.target.value)}
  />
</div>
<ul>
  {filteredWorkouts.map((workout, index) => (
    <li key={index}>
      <input
        type="checkbox"
        checked={selectedWorkouts.some(w => w.workoutName === workout.workoutName)}
        onChange={() => handleWorkoutSelection(workout)}
      />
      <span>{workout.workoutName}</span>
      <button onClick={() => handleViewWorkout(workout)}>View</button>
      /* this is the delete button that doesn't seem to work quite right*/
      <button onClick={() => handleRemoveWorkoutFromList(workout)}>Delete</button> 
    </li>
  ))}
</ul>

How can I fix this issue so that the UI updates without a page reload?

I'm happy to provide more of my code if needed (localStorage file, types, etc.). Thanks in advance.

What I expect: When I click the delete button next to a workout, it should immediately disappear from the list without a page reload.

What actually happens: The workout doesn't disappear from the UI until I reload the page. I’m storing the data in localStorage and deleting from that seems to work, but the UI doesn’t reflect that immediately.

What I’ve tried: I've tried updating the state directly with setSelectedWorkouts and setWorkoutsForToday, but it still doesn't update the UI immediately. I've also tried passing in states from the parent component to see if the issue was related to it being a child component but that didn't help. I also thought maybe the useEffect part needed to have something other than }, [selectedDate, savedWorkouts]); at the end and I've tried everything that made sense to me but to no avail. I've looked it up and found this similar issue and tried to use a filter function like the solution there said to and mine still doesn't work.


Solution

  • The following assignment statement makes the component impure which is the root cause of this undesired behaviour. It modifies the input which is in fact a side effect which should not do in a pure function. As you know, in React, every component must be pure. It should just calculate and return the result. Instead of making this side effect, you may use the state setter which you have already done by the call saveWorkouts(updatedWorkouts). Therefore please remove the assignment.

    const handleRemoveWorkoutFromList = (workout: Workout) => {
        ...
        savedWorkouts = updatedWorkouts;
        ...
      };
    

    The below sample program demoing the same issue. Please note the child component over here is pure.

    App.js

    import { useState } from 'react';
    
    export default function App() {
      const [someState, setSomeState] = useState('some initial value');
    
      return (
        <>
          <Child someStateParent={someState} setSomeStateParent={setSomeState} />
        </>
      );
    }
    
    function Child({ someStateParent, setSomeStateParent }) {
      const [someState, setSomeState] = useState(someStateParent);
    
      return (
        <>
          someState in the parent : {someStateParent}
          <br />
          <br />
          <label>Change Child's state here</label>
          <br />
          <input
            value={someState}
            onChange={(e) => setSomeState(e.target.value)}
          ></input>
          <br />
          <button
            onClick={() => {
              setSomeStateParent(someState);
            }}
          >
            Update Childs state in the parent
          </button>
        </>
      );
    }
    

    Test run

    On load of the App

    enter image description here

    On updating the state in the parent

    enter image description here

    Observation

    On updating the state in the parent, it works fine. The state in the parent changes with respect the same in the child.

    Let us modify the code as below. The below change has made the component Child an impure one since it has a side effect.

    ...
          <button
            onClick={() => {
              setSomeStateParent = setSomeState;
              setSomeStateParent(someState);
            }}
    ...
    

    Test run

    On loading the App

    enter image description here

    On updating the state in the parent

    enter image description here

    Observation

    On updating the state in the parent, it does not work. The state in the parent remains the same.