useState() hook removes fields from the object when setting new value

Viewed 97

Can't understand what I'm doing wrong here. I have a very simple object state:

const [itemInput, setItemInput] = useState({
    userId: userId,
    objectId: id,
    title: "",
    description: "",
    createdAt: new Date().getTime().toString(),
  })

Then, I have an input that changes the title field:

const onChange = (e) => {
    setItemInput((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value,
    }));
  };

And this is how onChange is used:

<input
        type="text"
        className="item-description-input"
        placeholder="Add description..."
        name="title"
        value={itemInput.title}
        onChange={onChange}
      />

GraphQL endpoint:

export const CREATE_ITEM = gql`
  mutation createItem($itemInput: ItemInput!) {
    createItem(itemInput: $itemInput) {
      userId
      objectId
      title
      description
      createdAt
    }
  }
`;

Submit function:

const onAddItemClick = (e) => {
    e.preventDefault();
    createItem({
      variables: {
        itemInput: itemInput,
      },
    }).then((res) => {
      console.log(res);
    });
  };

When submitting, I get:

message: "Variable "$itemInput" got invalid value { title: "ddd" }; Field "userId" of required type "MongoId!" was not provided." 

And this error is repeated for all the remaining fields but title.

If I console.log(itemInput) when the functional component is loaded, I get the initial state of the object. However if I add console.log(itemInput) after onChange, I get only {title: "some_added_text"}. onChange removes the other fields from the object, and, on submit, my graphQL endpoint returns an error that the other fields are not provided. This is very frustrating, as I seem to be doing everything correctly.

UPD: Added the components. Object component gets the list of objects. Each Object has Items. The problematic code is in the Items component when I want to add a new Item to that Object.

Object (parent) component:

import React, { useState, useReactiveVar, Fragment } from "react";
import { useMutation } from "@apollo/client";
import CREATE_OBJECT from "../../mutations/Object";
import { objectsVar } from "../../cache";

//Components
import ItemContainer from "./ItemContainer";

const Container = ({ data, userId, color }) => {
  const [objectInput, setobjectInput] = useState({
    title: "",
    userId: userId,
    createdAt: new Date().getTime().toString(),
    color: color,
  });

  const [createObject] = useMutation(CREATE_OBJECT);

  const onAddClick = (e) => {
    e.preventDefault();
    createObject({
      variables: {
        objectInput: objectInput,
      },
    }).then((res) => {
      setobjectInput({
        title: "",
        userId: userId,
        createdAt: new Date().getTime().toString(),
        color: color,
      });
    });
  };
  
  // This works perfectly fine!! 
  const onChange = (e) => {
    setobjectInput({ ...objectInput, [e.target.name]: e.target.value });
  };

  objectsVar(data);
  const objects = useReactiveVar(objectsVar);

  return (
    <div>
      <div>
        <input
          type="text"
          placeholder="Add description..."
          name="title"
          value={objectInput.title}
          onChange={onChange}
        />
      </div>
      <div onClick={onAddClick}>Add object</div>
      {objects && objects.getAllObjects.length === 0 ? (
        <p>No objects</p>
      ) : (
        <Fragment>
          {objects &&
            objects.getAllActiveSwimlines.map(({ _id, title, items }) => (
              <ItemContainer key={_id} id={_id} title={title} userId={userId} items={items} />
            ))}
        </Fragment>
      )}
    </div>
  );
};

export default Container;

Item (child) component:

import React, { useState, Fragment } from "react";
import ItemContainer from "../Item/ItemContainer";
import { useMutation } from "@apollo/client";
import { CREATE_ITEM } from "../../mutations/Item";

const ItemContainer = ({ id, title, items, userId }) => {
  const [itemInput, setItemInput] = useState({
    userId: userId,
    objectId: id,
    title: "initial state",
    description: "test desc",
    createdAt: new Date().getTime().toString(),
  });

  // This doesn't work!
  const onChange = (e) => {
    const { name, value } = e.target;
    setItemInput({
      ...itemInput,
      [name]: value,
    });
  };

  const [createItem] = useMutation(CREATE_ITEM);

  const onAddItemClick = (e) => {
    e.preventDefault();
    createItem({
      variables: {
        itemInput: itemInput,
      },
    }).then((res) => {
      setItemInput({
        userId: userId,
        objectId: id,
        title: "",
        description: "",
        createdAt: new Date().getTime().toString(),
      });

      console.log(res);
    });
  };

  const newItemInput = (
    <div>
      <input
        type="text"
        placeholder="Add description..."
        name="title"
        value={itemInput.title}
        onChange={onChange}
      />
      <div onClick={onAddItemClick}>Add item</div>
    </div>
  );

  const itemContent = (
    <div>
      <Fragment>
        {items &&
          items.getObjectItems.map(({ _id, title }) => (
            <Item key={_id} id={_id} title={title} />
          ))}
      </Fragment>
    </div>
  );

  return (
    <div>
      {newItemInput}
      {itemContent}
    </div>
  );
};

export default ItemContainer;

What am I missing here?

1 Answers

Because you access e.target.name and e.target.value inside your update function, they don't get evaluated immediately, and by the the time they do, the synthetic event object has been reused. If you want to keep the rest of the code as it is, you can fix it by calling e.persist() at the start of onChange, or copy the relevant information from the event:

  const onChange = (e) => {
    const {name, value} = e.target
    setItemInput((prevState) => ({
      ...prevState,
      [name]: value,
    }));
  };

Alternatively, you can let go of the update function and just set the state directly, (which was also suggested in the other answer that got deleted):

  const onChange = (e) => {
    setItemInput({
      ...itemInput,
      [e.target.name]: e.target.value,
    });
  };

Related