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?
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:
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)[]>>
.
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]
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.