maison.m
maison.m

Reputation: 863

React - How do I manage the state of multiple dynamically created controlled inputs?

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

Answers (2)

maison.m
maison.m

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

gergana
gergana

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

Related Questions