Explanation
In isolation of the code you have presented, all that can really be said is that selectedStopwatchIndex is initialized to the value of -1 and there is a fundamental difference between what/how this -1 index value is used between Array.splice and how you use it in Array.filter.
Specifically, using the .splice method when a negative starting index is passed it is an offset index from the end of the array.
start
Zero-based index at which to start changing the array, converted to an integer.
- Negative index counts back from the end of the array — if
-array.length <= start < 0, start + array.length is used.
- If
start < -array.length, 0 is used.
- If
start >= array.length, no element will be deleted, but the method will behave as an adding function, adding as many elements as provided.
- If
start is omitted (and splice() is called with no arguments), nothing is deleted. This is different from passing undefined, which is converted to 0.
So passing -1 will actually effect a change in the array.
const index = -1;
const foo = [0,1,2,3,4,5];
const newFoo = [...foo]
newFoo.splice(index, 1);
console.log(JSON.stringify(newFoo)); // [0,1,2,3,4]
The last array element was removed!
Contrast this with using the .filter method where you are comparing each index value against selectedStopwatchIndex. Since all array indices are non-negative integers, none of them will ever equal -1 and omit the element.
const index = -1;
const foo = [0,1,2,3,4,5];
const newFoo = [...foo].filter((_, i) => i !== index);
console.log(JSON.stringify(newFoo)); // [0,1,2,3,4,5]
Here you see that since no index was equal to -1 and all elements were kept.
Technically there should no difference between the two implementations you are comparing, but the basic issue in your code doesn't appear to utilize selectedStopwatchIndex correctly. If/when you update selectedStopwatchIndex to match the index of the currently selected/active stopwatch, then the result should be the same between the two implementations.
const index = 3;
const foo = [0,1,2,3,4,5];
const newFoo = [...foo]
newFoo.splice(index, 1);
const newFoo2 = [...foo].filter((_, i) => i !== index);
console.log(JSON.stringify(newFoo)); // [0,1,2,4,5]
console.log(JSON.stringify(newFoo2)); // [0,1,2,4,5]
Why didn't the array.filter work though?
In StopwatchList when rendering the list of stopwatches you assign the mapped array index to a data-x attribute
let listItems = stopwatches.map((stopwatch, i) => (
<li
key={i}
data-stopwatch-id={i} // <--
onClick={(e) => handleStopwatchItemClick(e)}
>
Stopwatch {i}
</li>
));
and access this attribute value in a click handler
function handleStopwatchItemClick(e) {
console.log(`Opening stopwatch ${e.currentTarget.dataset.stopwatchId}`);
setSelectedStopwatchIndex(e.currentTarget.dataset.stopwatchId); // <--
}
The issue here is that e.currentTarget.dataset.stopwatchId is a string type, e.g. the values are "0", "1", etc... and the filter callback uses strict equality, so 1 === "1" will always evaluate false. Using loose equality will however attempt to coerce the type for comparison.
Example:
console.log('1 === "1"', 1 === "1"); // false!
console.log('1 == "1"', 1 == "1"); // true
Possible solutions available to you here are:
Convert e.currentTarget.dataset.stopwatchId back to a number before passing it back to the caller so the strict equality check will pass as expected.
setSelectedStopwatchIndex(Number(e.currentTarget.dataset.stopwatchId));
Convert selectedStopwatchIndex to a number type at the time of comparison, ensuring the types match between operands.
removeStopwatch={() => {
setStopwatches((stopwatches) => {
return stopwatches.filter(
(_, i) => i !== Number(selectedStopwatchIndex)
);
});
}}
Or forego the entire data-x attribute altogether and just simply pass the mapped array index, which will already be a type match.
function handleStopwatchItemClick(i) {
console.log(`Opening stopwatch ${i}`);
setSelectedStopwatchIndex(i);
}
const listItems = stopwatches.map((stopwatch, i) => (
<li
key={i}
onClick={() => handleStopwatchItemClick(i)}
>
Stopwatch {i}
</li>
));
A word of advice here: maintaining your state type invariants, i.e. number for selectedStopwatchIndex, would help eliminate issues like this. Moving to Typescript makes this even easier as you'd get a build-time warning when attempting to pass any non-number type instead of a run-time error, or worse, no error at all and buggy code that doesn't crash like what you had.
Additionally
FWIW it's a general React anti-pattern to use the mapped array index also as the mapped React elements' keys, especially since you are mutating the array (i.e. adding and removing array elements). You should generally use a value that is intrinsic the mapped elements. GUIDs typically make great React keys. In your case here I'd suggest using a Stopwatch's name property.
An example rewrite of StopwatchList might look something like:
import { nanoid } from "nanoid";
import Stopwatch from "./Stopwatch";
function StopwatchList({
stopwatches,
setStopwatches,
setSelectedStopwatchIndex,
}) {
function handleAddStopwatch() {
setStopwatches((stopwatches) =>
// add new stop with generated GUID name value
stopwatches.concat(new Stopwatch(nanoid()))
);
}
return (
<>
<ul>
{stopwatches.map((stopwatch, i) => (
<li
// Use stopwatch name GUID for React key
key={stopwatch.name}
// Call setSelectedStopwatchIndex directly and pass current index
onClick={() => setSelectedStopwatchIndex(i)}
>
{/* Use stopwatch name GUID for easy identification */}
Stopwatch {stopwatch.name}
</li>
))}
</ul>
<button onClick={handleAddStopwatch}>Add stopwatch</button>
</>
);
}
useEffect(() => console.log(stopwatches), [stopwatches]);to see if react recognizes the state change. If so, then throw the same thing in theStopwatchDetailscomponent as well to see if it receives the updated stateselectedStopwatchIndexwhen you press the remove button? Please edit to include complete details of your problem and the exact reproduction steps, along with any debugging logs and details from investigation.newArray.filter(...)MainScreencomponent, it prints thestopwatcheslist whenever I press the remove button, but the list doesn't actually change. I can't put it inStopwatchDetailsbecauseStopwatchDetailsisn't aware of thestopwatcheslist, it only knows about the current Stopwatch