Jatin saini
Jatin saini

Reputation: 55

Multiple rows in a state refers to a single point in react state

I am trying to update a state which is of type --> [{}, {},...]. When I update the data in single object it works perfectly. But when I add another row and add some data in that object then both rows will have same data object e.g.

[{"name": "John"}, {"name": "John"}]

But, I want it to be like

[{"name": "John"}, {"name": "Martha"}]

These are just examples not actual object. Object is given below.

The code I am using for that is :

const WorkTab:React.FC<{workHandler: any, workExp: any, err: any}> = ({workExp, err}) => {
  const [workIndex, setWorkIndex] = useState(workExp.length === 0 ? 1 : workExp.length);
  const [work, setWork] = useState(workExp)

  const { userInfo } = useSelector((state: any) => state.userDetails);

  const handleChange = (e: any, input: string, i: number, value?: any) => {
    const [ ...tempWork ] = work;
    if (work.length === 0 || tempWork[i] === undefined) {
      tempWork.push(workTemplate);
    }

    if (input === "company") {
      tempWork[i][input] = e.target.value;
    } else if (input === "title") {
        tempWork[i][input] = value;
    } else {
      tempWork[i][input] = e.toISOString();
    }
    setWork([...tempWork]);
  };

  const WorkGrid = (n: number) => {
    return (
    <Grid container spacing={3} style={{ marginTop: "5px" }}>
      <Grid item xs={12} md={4}>
        <TextField
          sx={{ width: { lg: "98%", md: "100%" } }}
          label="Firm Name"
          variant="outlined"
          onChange={(e) => handleChange(e, "company", n)}
          error={err && work[n]?.company === ""}
          defaultValue={userInfo?.work_experiences[n]?.company}
        />
      </Grid>

      <Grid item xs={12} md={5}>
        <Autocomplete
          sx={{ width: "100%" }}
          disableClearable
          options={preferences.jobFunction}
          defaultValue={userInfo?.work_experiences[n]?.title}
          onChange={(e, value) => handleChange(e, "title", n, value)}
          renderInput={(params) => (
            <TextField {...params} label="Job Function" />
          )}
        />
      </Grid>

      <Grid
        item
        xs={12}
        md={3}
        sx={{
          display: "flex",
          alignItems: "center",
          justifyContent: "space-between",
        }}
      >
        <LocalizationProvider dateAdapter={AdapterDateFns}>
          <MobileDatePicker
            label="Start Date"
            views={["year", "month"]}
            inputFormat="MM/yyyy"
            value={new Date(work[n]?.start_date)}
            onChange={(e, newValue) => handleChange(e, "start_date", n, newValue)}
            renderInput={(params) => <TextField {...params} />}
          />
        </LocalizationProvider>
      </Grid>
    </Grid>
    )};

  return (
    <div>
      {[...Array(workIndex)].map((n: number, index: number) => (
        <div key={index}>{WorkGrid(index)}</div>
      ))}

      <div style={{display: "flex", justifyContent: "space-between"}} className='state-btns'>

      <Button
        sx={{ color: "#000", textTransform: "capitalize" }}
        onClick={() => setWorkIndex((prev: number) => prev + 1)}
      >
        Add other work experience +
      </Button>

      {workIndex !== 1 && <Button onClick={() => setWorkIndex((prev: number) => prev - 1)}><Delete style={{color: "black"}} /></Button>}
      </div>
    </div>
  );
};

Also the workTemplate is:

const workTemplate = {
  title: "",
  company: "",
  industry: "",
  position: "",
  city: "",
  start_date: "",
}

I am not able to figure out why it is behaving like this.

Can anyone please explain this behavior?

Upvotes: 2

Views: 302

Answers (1)

Drew Reese
Drew Reese

Reputation: 203512

You have an issue of state mutation. Every element you push into the work state is a reference to the workTemplate object. The handleChange handler then is mutating the state object references.

const handleChange = (e: any, input: string, i: number, value?: any) => {
  const [ ...tempWork ] = work;
  if (work.length === 0 || tempWork[i] === undefined) {
    tempWork.push(workTemplate);          // <-- reference to `workTemplate` object
  }

  if (input === "company") {
    tempWork[i][input] = e.target.value;  // <-- mutate reference
  } else if (input === "title") {
    tempWork[i][input] = value;           // <-- mutate reference
  } else {
    tempWork[i][input] = e.toISOString(); // <-- mutate reference
  }
  setWork([...tempWork]);
};

Use a functional state update to update from the previous state. Create a shallow copy of all state/objects being updated. You will find it helpful/useful to also copy any template objects as a way to keep them from being accidentally mutated.

Example:

const handleChange = (e: any, input: string, i: number, value?: any) => {
  const tempWork = work.slice(); // <-- shallow copy

  if (!tempWork.length || !tempWork[i]) {
    tempWork.push({ ...workTemplate }); // <-- shallow copy template
  }

  let newValue = e.toISOString();
  if (input === "company") {
    newValue = e.target.value;
  } else if (input === "title") {
    newValue = value;
  }

  const newWork = tempWork.map((item, index) => index === i
    ? {
      ...item,          // <-- shallow copy element
      [input]: newValue // <-- update property
    }
    : item
  );

  setWork(newWork);
};

Upvotes: 1

Related Questions