Reputation: 863
So in my case, I am mapping over a returned user object, and creating essentially a form for each user. Each form represents an 'Add Hours Worked' set of inputs for each respective user.
For example here is an abbreviated version of my returned user array:
employees: [
{
name: Jason Doe,
email: [email protected],
},
{
name: Susan Doe,
email: [email protected],
},
{
name: Jon Doe,
email: [email protected]
}
]
And from these users I create an individual 'Add Hours' form for each of them inside of the React component like so:
{employees.map((employee, i) => (
<EmployeeTableItem key={i}>
<p>{employee.fullName}</p>
<input type="number" placeholder="Reg. Hours" />
<input type="number" placeholder="OT Hours" />
<input type="date" placeholder="From Date" />
<input type="date" placeholder="To Date" />
<div>
<AddHoursButton bordercolor="secondaryColorLight" type="button">
Add Hours
</AddHoursButton>
</div>
</EmployeeTableItem>
))}
In that example I have not yet set the value
and onChange
of the inputs, I am aware of that. I do know how to create controlled inputs, but usually when I create them they are not from dynamic sets of data like this. In the above example, each employee would need their own state to manage their respective inputs, and that state needs to be created after the employee data loads in from the parent component. The employee data in this example comes via props.
My question is how do I manage the state of all of the dynamic inputs? Each input would need a specific value to hold and set in state, however that state is not created at this point, because the inputs are all different depending on the incoming data set.
This is my typical implementation of a controlled input with React Hooks:
state:
const [postData, setPostData] = useState({
firstName: '',
lastName: '',
email: ''
});
update function:
const updatePostData = (value, key) => {
setPostData(prevData => {
return {
...prevData,
[key]: value
};
});
};
inputs:
<input value={postData.firstName} onChange={(e) => updatePostData(e.target.value, 'firstName')} />
<input value={postData.lastName} onChange={(e) => updatePostData(e.target.value, 'lastName')} />
<input value={postData.email} onChange={(e) => updatePostData(e.target.value, 'email')} />
This example is pretty straightforward and simple to do. I already know the types of inputs I would need to create both in the component and in state. For example, a user sign up form. I already know what inputs I need, so I could hard code the values for them into state.
I am leaning towards this being a no-brainer to some degree, and I think I am over thinking how to do this. Regardless, I appreciate the insight and advice in advance!
Upvotes: 0
Views: 474
Reputation: 863
I ended up initializing the postData
state dynamically on mount via useEffect()
. Inside of the useEffect()
function, I mapped over the employees
array and created an object for each employee with the property keys required for the inputs. Here is that implementation of creating the object in state dynamically:
const AddHours = ({ employees }) => {
const [postData, setPostData] = useState({);
useEffect(() => {
let postDataObj = {};
employees.forEach(employee => {
postDataObj = {
...postDataObj,
[employee._id]: {
regHours: 0,
otHours: 0,
fromDate: 'mm/dd/yy',
toDate: 'mm/dd/yy'
}
};
});
setPostData(postDataObj);
}, [employees]);
}
After each iteration of the employees
array in the forEach
function, I create an object with the employee._id
and inside of that object, setup the keys required to capture the inputs. Each employee
object can be referenced by the _id
that comes along with each employee
data. After every loop, the newly created object, is added to postDataObj
and to hold the previous version of that object, I spread it (...postDataObj
) to get a shallow copy.
When the forEach
function is completed, postDataObj
is set into state via setPostData(postDataObj)
. This effect will be ran anytime the employees
props change.
Finally, this is what a console.log
of postData
looks like after all of this:
5e3db85dfe149131cd7c9c77: {regHours: 0, otHours: 0, fromDate: "mm/dd/yy", toDate: "mm/dd/yy"}
5e3db870fe149131cd7c9c78: {regHours: 0, otHours: 0, fromDate: "mm/dd/yy", toDate: "mm/dd/yy"}
5e3db87ffe149131cd7c9c79: {regHours: 0, otHours: 0, fromDate: "mm/dd/yy", toDate: "mm/dd/yy"}
5e3db896fe149131cd7c9c7a: {regHours: 0, otHours: 0, fromDate: "mm/dd/yy", toDate: "mm/dd/yy"}
5e5d379c03e7d104bb98fa39: {regHours: 0, otHours: 0, fromDate: "mm/dd/yy", toDate: "mm/dd/yy"}
5e5d395503e7d104bb98fa3a: {regHours: 0, otHours: 0, fromDate: "mm/dd/yy", toDate: "mm/dd/yy"}
5e5d3dcd5cd2850882c105ba: {regHours: 0, otHours: 0, fromDate: "mm/dd/yy", toDate: "mm/dd/yy"}
Now I have the the objects needed to handle the controlled inputs within the component.
This is the updatePostData
function that the inputs will utilize to update state:
const updatePostData = (id, key, value) => {
setPostData(prevData => {
return {
...prevData,
[id]: {
...postData[id],
[key]: value
}
};
});
};
This is pretty straightforward. The inputs pass in an id, key, value
argument, that the function will use to find the employee._id
key in the postDataObj
object, and the [key]: value
is obviously the value and target key passed in from the input onChange()
. Note that the prevData
is spread into the new object, and then re-saved into state. This is necessary to persist the unchanged values, but also we are creating a new copy of the state, and not mutating it directly. The ...postData[id]
is spreading the previous key/values of the employee's object. This persists their input data object through updates.
Finally, here are the inputs:
{ Object.keys(postData).length !== 0 ?
{employees.map((employee, i) => (
<EmployeeTableItem>
<p>{employee.fullName}</p>
<input
value={postData[employee._id].regHours}
onChange={e =>
updatePostData(employee._id, 'regHours', e.target.value)
}
type="number"
placeholder="Reg. Hours"
/>
<input
value={postData[employee._id].otHours}
onChange={e =>
updatePostData(employee._id, 'otHours', e.target.value)
}
type="number"
placeholder="OT Hours"
/>
<input
value={postData[employee._id].fromDate}
onChange={e =>
updatePostData(employee._id, 'fromDate', e.target.value)
}
type="date"
placeholder="From Date"
/>
<input
value={postData[employee._id].toDate}
onChange={e =>
updatePostData(employee._id, 'toDate', e.target.value)
}
type="date"
placeholder="To Date"
/>
<div>
<AddHoursButton
bordercolor="secondaryColorLight"
type="button"
>
Add Hours
</AddHoursButton>
</div>
</EmployeeTableItem>
))} : null
}
Since we are creating the postData
object when the component mounts, if we initialize the inputs, they won't have access to the postData
state created in the useEffect()
initially on mount. To get around this, the inputs are only rendered after the postData
state is created via a ternary operator. Although the postData
is created very fast, it still happens slightly after the component renders initially, so therefore the inputs don't get access to it right off the bat.
And that's about that for me on this. I am sure this could somehow be turned in a reusable hook in some way, but for now this gets me to the next step.
Upvotes: 0
Reputation: 691
You can use an id for each input and employee, so when you set data in state it is saved by id there.
employees: [
{
id: 1,
name: Jason Doe,
email: [email protected],
},
{
id: 2,
name: Susan Doe,
email: [email protected],
},
{
id: 3,
name: Jon Doe,
email: [email protected]
}]
So state would be object of objects. Can you try setting your initial state to empty object
const [postData, setPostData] = useState({})
Pass id onChange and use it to set it into state.
<input id={postData.id} value={postData.firstName} onChange={(e) => updatePostData(e.target.value, postData.id, 'firstName')} />
<input id={postData.id} value={postData.lastName} onChange={(e) => updatePostData(e.target.value, postData.id, 'lastName')} />
<input id={postData.id} value={postData.email} onChange={(e) => updatePostData(e.target.value, postData.id, 'email')} />
Update function
const updatePostData = (value, key, id) => {
setPostData(prevData => {
return {
...prevData,
...prevData[id] ? prevData[id]: {
...prevData[id]
key: value
} : prevData[id]: { key: value }
};
});
};
Upvotes: 1