I come up with a solution for change the properties in a second level nested state in React that is not scalable and it doesn't seem quite efficient. How can you do a refactor of handleOnChange
method in order to change reps
and weight
?
import React, { useState } from "react";
const workout = {
id: "123-234sdf-1213",
name: "wo name",
done: false,
exercises: [
{
name: "back squat",
sets: [
{
number: 0,
reps: 0,
weight: 0,
done: false,
},
{
number: 1,
reps: 0,
weight: 0,
done: false,
},
{
number: 2,
reps: 0,
weight: 0,
done: false,
},
],
},
{
name: "leg press",
sets: [
{
number: 0,
reps: 0,
weight: 0,
done: false,
},
{
number: 1,
reps: 0,
weight: 0,
done: false,
},
{
number: 2,
reps: 0,
weight: 0,
done: false,
},
],
},
],
};
export default function App() {
const [workoutState, setWorkoutState] = useState(workout);
const handleOnChange = (e, exIndex, setIndex) => {
const value = e.target.value ? parseFloat(e.target.value) : "";
const exercises = [...workoutState.exercises];
exercises[exIndex].sets[setIndex] = {
...exercises[exIndex].sets[setIndex],
[e.target.name]: value,
};
setWorkoutState({
...workoutState,
exercises,
});
};
return (
<div className="App">
<h1>{workoutState.name}</h1>
{workoutState.exercises.map((ex, exIndex) => (
<>
<h2>{ex.name}</h2>
{ex.sets.map((set, setIndex) => (
<div key={`${ex.name}_${setIndex}`}>
<div>
reps:{" "}
<input
value={set.reps}
name="reps"
type="number"
onChange={(e) => handleOnChange(e, exIndex, setIndex)}
/>
</div>
<div>
weight:{" "}
<input
value={set.weight}
name="weight"
type="number"
onChange={(e) => handleOnChange(e, exIndex, setIndex)}
/>
</div>
</div>
))}
</>
))}
</div>
);
}
I would say you are on the right track with how you want to update the nested state, but you do have a state mutation in your current handleOnChange
implementation. You are mutating exercises[exIndex]
.
const handleOnChange = (e, exIndex, setIndex) => {
const value = e.target.value ? parseFloat(e.target.value) : "";
const exercises = [...workoutState.exercises];
exercises[exIndex].sets[setIndex] = { // <-- mutates exercises[exIndex]
...exercises[exIndex].sets[setIndex],
[e.target.name]: value,
};
setWorkoutState({
...workoutState,
exercises,
});
};
You should shallow copy each nested level of state you intend to update.
I recommend using functional state updates so if for any reason more than a single state update is enqueued during a render cycle the update will work from the previous update versus from using the state from the render cycle the update was enqueued.
I also prefer making the handler a curried function. It takes the indices as arguments and returns a function that consumes the onChange
event object. This allows you to remove the anonymous function declaration and need to proxy the event object yourself when attaching the handler, i.e. onChange={handleOnChange(exIndex, setIndex)}
versus onChange={e => handleOnChange(e, exIndex, setIndex)}
const handleOnChange = (eIndex, sIndex) => e => {
const { name, value } = e.target;
setWorkoutState((workoutState) => ({
...workoutState,
exercises: workoutState.exercises.map((exercise, exerciseIndex) =>
exerciseIndex === eIndex
? {
...exercise,
sets: exercise.sets.map((set, setIndex) =>
setIndex === sIndex
? {
...set,
[name]: Number(value)
}
: set
)
}
: exercise
)
}));
};
The inputs. I added a step
attribute and attach the curried handler. I also placed each input in a label
element for accessibility; you can click the label and focus the field.
<div>
<label>
reps:{" "}
<input
value={set.reps}
name="reps"
step={1}
type="number"
onChange={handleOnChange(exIndex, setIndex)}
/>
</label>
</div>
<div>
<label>
weight:{" "}
<input
value={set.weight}
name="weight"
step={0.1}
type="number"
onChange={handleOnChange(exIndex, setIndex)}
/>
</label>
</div>
Full code:
const workout = {
id: "123-234sdf-1213",
name: "wo name",
done: false,
exercises: [
{
name: "back squat",
sets: [
{
number: 0,
reps: 0,
weight: 0,
done: false
},
{
number: 1,
reps: 0,
weight: 0,
done: false
},
{
number: 2,
reps: 0,
weight: 0,
done: false
}
]
},
{
name: "leg press",
sets: [
{
number: 0,
reps: 0,
weight: 0,
done: false
},
{
number: 1,
reps: 0,
weight: 0,
done: false
},
{
number: 2,
reps: 0,
weight: 0,
done: false
}
]
}
]
};
export default function App() {
const [workoutState, setWorkoutState] = useState(workout);
const handleOnChange = (eIndex, sIndex) => (e) => {
const { name, value } = e.target;
setWorkoutState((workoutState) => ({
...workoutState,
exercises: workoutState.exercises.map((exercise, exerciseIndex) =>
exerciseIndex === eIndex
? {
...exercise,
sets: exercise.sets.map((set, setIndex) =>
setIndex === sIndex
? {
...set,
[name]: Number(value)
}
: set
)
}
: exercise
)
}));
};
return (
<div className="App">
<h1>{workoutState.name}</h1>
{workoutState.exercises.map((ex, exIndex) => (
<>
<h2>{ex.name}</h2>
{ex.sets.map((set, setIndex) => (
<div key={`${ex.name}_${setIndex}`}>
<h3>Set {setIndex}</h3>
<div>
<label>
reps:{" "}
<input
value={set.reps}
name="reps"
step={1}
type="number"
onChange={handleOnChange(exIndex, setIndex)}
/>
</label>
</div>
<div>
<label>
weight:{" "}
<input
value={set.weight}
name="weight"
step={0.1}
type="number"
onChange={handleOnChange(exIndex, setIndex)}
/>
</label>
</div>
</div>
))}
</>
))}
</div>
);
}