Skip to content
Advertisement

React Controlled Form with Child /Parent component

I’m building a controlled form with dynamic fields. The Parent component get data from a redux store and then set state with the values. I don’t want to make it with too much code lines so I turn the dynamic fields into a component. States stay in the parent component and I use props to pass the handlechange function.

Parent :

function EditAbout(props) {
  const [img, setImg] = useState("");
  const [body, setBody] = useState(props.about.body);
  const [instagram, setInstagram] = useState(props.about.links.instagram);
  const [linkedin, setLinkedIn] = useState(props.about.links.linkedin);
  const [press, setPress] = useState(props.about.press)

  const handleSubmit = (e) => {
   // Submit the change to redux
  };

// set states with redux store
  useEffect(() => {
    setBody(props.about.body);
    setInstagram(props.about.links.instagram);
    setLinkedIn(props.about.links.linkedin);
    setPress(props.about.press);
  }, []);

  const handleChangeChild = (e, index) =>  {
    e.preventDefault();
    let articles = press
    const {value, name } = e.target
    if (name === "title") {
      articles[index].title = value;
    } else {
      articles[index].link = value;
    }
    setPress(articles)
    console.log(articles[index])
  }

  return (
    <Box>
      <h1>CHANGE ABOUT ME</h1>
      <Input
        label="Image"
        name="img"
        type="file"
        variant="outlined"
        margin="normal"
        onChange={(e) => setImg(e.target.files)}
      />
      <Input
        label="body"
        value={body}
        name="body"
        onChange={(e) => setBody(e.target.value)}
        variant="outlined"
        multiline
        rowsMax={12}
        margin="normal"
      />
      <Input
        label="instagram"
        value={instagram}
        name="instagram"
        variant="outlined"
        margin="normal"
        onChange={(e) => setInstagram(e.target.value)}
      />
      <Input
        label="Linkedin"
        value={linkedin}
        name="linkedin"
        variant="outlined"
        margin="normal"
        onChange={(e) => setLinkedIn(e.target.value)}
      />
      <Child press={press} onChange={handleChangeChild} />
      {props.loading ? (
        <CircularProgress color="black" />
      ) : (
        <Button onClick={handleSubmit} variant="contained">
          Send
        </Button>
      )}
    </Box>
  );
}

Child :

function Child(props) {
  const { press, onChange } = props;

  const inputsMarkup = () =>
    press.map((article, index) => (
      <div key={`press${index}`} style={{ display: "flex" }}>
        <input
          name="title"
          value={press[index].title}
          onChange={(e) => onChange(e, index)}
        />
        <input
          name="link"
          value={press[index].link}
          onChange={(e) => onChange(e, index)}
        />
        <button>Delete</button>
      </div>
    ));

  return (
    <div>
      <h1>Press :</h1>
      {inputsMarkup()}
    </div>
  );
}

Everything is fine when I’m typing in the Parent inputs. But when I’m using Child fields state update for one character but come back at its previous state right after. It also doesn’t display the character change. I can only see it in the console. Thanks you in advance for your help

Answer

The problem is that you’re mutating the state directly. When you create the articles variable (let articles = press) you don’t actually create a copy and articles doesn’t actually contain the value. It’s only a reference to that value, which points to the object’s location in memory.

So when you update articles[index].title in your handleChangeChild function, you’re actually changing the press state too. You might think that’s fine, but without calling setPress() React will not be aware of the change. So, although the state value is changed, you won’t see it because React won’t re-render it.

You need to create a copy of the press array using .map() and create a copy of the updated array element. You can find the updated handleChangeChild() below:

const handleChangeChild = (e, index) => {
  e.preventDefault();

  const { value, name } = e.target;

  setPress(
    // .map() returns a new array
    press.map((item, i) => {
      // if the current item is not the one we need to update, just return it
      if (i !== index) {
        return item;
      }

      // create a new object by copying the item
      const updatedItem = {
        ...item,
      };

      // we can safely update the properties now it won't affect the state
      if (name === 'title') {
        updatedItem.title = value;
      } else {
        updatedItem.link = value;
      }

      return updatedItem;
    }),
  );
};

Advertisement