reactjstypescriptnext.jst3

Type of setState error in child component not allowing appending to state


I recently switched over to TypeScript in NextJS (via Create T3 App). As a component of my app, I would like to update the state once a Prisma mutation is made.

I passed through the setItems (initialised through useState) to a child component and this continues to fail with

Type error: Argument of type '(prev: Example[]) => (Example | undefined)[]' is not assignable to parameter of type 'SetStateAction<Example[]>'.

My code is as follows:

interface FormProps {
  setItems: Dispatch<SetStateAction<Example[]>>;
}

const Form: FC<FormProps> = ({ setItems }) => {
  const [message, setMessage] = useState("");
  //setItems([])
  const { mutate: createExample } = api.example.createExample.useMutation({
    onSuccess: (data) => {
      setItems((prev) => [...prev, data]);
    },
  });

  return (
    <form
      className="flex gap-2"
      onSubmit={(event) => {
        event.preventDefault();
        createExample({
          text: message,
        });
        setMessage("");
      }}
    >
      <input
        type="text"
        placeholder="Your message..."
        minLength={2}
        maxLength={100}
        value={message}
        onChange={(event) => setMessage(event.target.value)}
      />
      <button
        type="submit"
        className="rounded-md border-2 border-zinc-800 p-2 focus:outline-none"
      >
        Submit
      </button>
    </form>
  );
};

With setItems initialised in my main page:

const Demo: NextPage = () => {
  const [items, setItems] = useState<Example[]>([]);
  const { data: session, status } = useSession();

  const user = api.example.getAll.useQuery(["getAll"], {
    onSuccess: (user) => {
      setItems(user.examples);
    },
  });

  const { mutate: deleteExample } = api.example.deleteExample.useMutation({
    onSuccess(example) {
      setItems((prev) => prev.filter((item) => item.id !== example.id));
    },
  });

  return (
    <>
      {session ? (
        <>
          <p>Hello {session ? session.user?.name : null}</p>
          <button
            type="button"
            onClick={() => {
              signOut().catch(console.log);
            }}
          >
            Logout
          </button>
          <br />
          <ul>
            {user.data
              ? items.map((example) => (
                  <li>
                    {example.text}{" "}
                    <button
                      onClick={() => {
                        deleteExample({ id: example.id });
                      }}
                    >
                      X
                    </button>
                  </li>
                ))
              : null}
          </ul>
          <br />
          <Form setItems={setItems} />
        </>
      ) : (
        <button
          type="button"
          onClick={() => {
            signIn("email").catch(console.log);
          }}
        >
          Login with Email
        </button>
      )}
    </>
  );
};

It seems to get the type correctly, knowing that it’s an ‘Example’. However, throws a type error because I try to access the previous state.

How does one combine states while keeping types consistent?


Solution

  • I think the problem is that the data variable in the onSuccess method might be undefined.

    And typescript doesn't want to assign an array that might have undefined into an array that doesn't.

    Now, how do we fix this? Well it depends.

    Why would data be undefined? There are 3 possibilities:

    1. data is always undefined (so if you hover over data, you should see data: undefined).

    If this is the case, you either have some other bug that's causing data to be undefined, or you expect to have a list with undefined in it, if you do, tell typescript that's the case by changing useState<Example[]> to useState<(Example|undefined)[]> and Dispatch<SetStateAction<Example[]>> to Dispatch<SetStateAction<(Example|undefined)[]>>.

    1. data is sometimes undefined (data: Example|undefined).

    If Data is sometimes undefined, there are 2 further possibilities. Either you want to add undefined to the list anyway, or you want to avoid adding undefined to the list.

    If you do want to add undefined to the list, just follow the previous step.

    If you do not want to add undefined to the list, just make sure it isn't undefined before running the setItems function:

        onSuccess: (data) => {
          if (data)
            setItems((prev) => [...prev, data]);
        },
    

    If after this you still recieve the error, make the following change: [...prev, data as Example]

    1. Data is never undefined.

    If you know for a fact data will never be undefined, but it's still defined as if it could be, and there is no way of changing this fact, you can use the previous case's code to tell typescript that:

    setItems((prev) => [...prev, data as Example]);
    

    One of the above cases should solve the issue.