I am struggling with useFormStatus in React 19.
I am using it in my nextJS app.
This is my page.tsx
import { getCardsFromUser } from '@/app/u/cards/actions'
import { getClientsFromUser } from '@/app/u/clients/actions'
import Header from '@/components/header'
import { Button } from '@/components/ui/button'
import { requireUser } from '@/utils/auth'
import CreateCardDialog from './create'
import { DataTable } from './table'
import { columns } from './table/columns'
const CardsPage = async () => {
requireUser()
const { data: cards } = await getCardsFromUser()
const { data: clients } = await getClientsFromUser()
return (
<>
<Header title='Cards'>
<CreateCardDialog clients={clients}>
<Button>Add card</Button>
</CreateCardDialog>
</Header>
<DataTable columns={columns} data={cards} />
</>
)
}
export default CardsPage
This is my CreateCardDialog
'use client'
import { createCard } from '@/app/u/cards/actions'
import SubmitButton from '@/components/submitbutton'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { Tables } from '@/types/supabase'
import { InfoCircledIcon } from '@radix-ui/react-icons'
import { Euro } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { useFormState } from 'react-dom'
type CreateClientDialogProps = {
children: React.ReactNode
clients: Tables<'clients'>[] | null
}
const initialState = undefined
type ErrorType = string | undefined
const CreateCardDialog = ({ children, clients }: CreateClientDialogProps) => {
const [open, setOpen] = useState(false)
const formRef = useRef<HTMLFormElement>(null)
const [state, formAction] = useFormState(createCard, initialState)
const [errorMessage, setErrorMessage] = useState<ErrorType>(undefined)
const [formData, setFormData] = useState<FormData>(new FormData())
const [customEndDate, setCustomEndDate] = useState(false)
const handleSwitchChange = () => {
setCustomEndDate(!customEndDate)
}
const today = new Date()
const oneYearFromNow = new Date(
today.getFullYear() + 1,
today.getMonth(),
today.getDate()
)
const formattedDate = oneYearFromNow.toISOString().split('T')[0]
useEffect(() => {
if (state?.status === 'success' && open === true) {
setOpen(false)
setErrorMessage(undefined)
} else if (state?.status === 'error' && open === true) {
setErrorMessage(state.message)
} else if (state?.status === 'error' && open === false) {
state.message = ''
state.errors = {}
setErrorMessage(undefined)
}
}, [state, open])
return (
<>
{open ? <p className='text-lg'>JA</p> : <p className='text-lg'>NEE</p>}
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<p>
{clients && clients.length === 0 ? (
<div>
Please add
<Link href='/u/clients' className='border-b border-b-black'>
clients
</Link>
first before you add a card.
</div>
) : (
<form ref={formRef} action={formAction}>
<div className='mb-4'>
<Label htmlFor='client_id' className='mb-2'>
Client
</Label>
<Select name='client_id' required>
<SelectTrigger className='w-[240px]'>
<SelectValue placeholder='Select client' />
</SelectTrigger>
<SelectContent>
{clients?.map((client) => (
<SelectItem key={client.id} value={client.id}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
{state?.errors?.client_id && (
<p className='py-2 text-xs text-red-500'>
{state.errors.client_id}
</p>
)}
</div>
<div className='mb-4'>
<Label htmlFor='hours' className='mb-2'>
Hours
</Label>
<Input type='number' name='hours' id='hours' required />
{state?.errors?.hours && (
<p className='py-2 text-xs text-red-500'>
{state.errors.hours}
</p>
)}
</div>
<div className='mb-4'>
<Label htmlFor='price' className='mb-2'>
Price
</Label>
<div className='relative flex items-center max-w-2xl '>
<Euro className='absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 transform' />
<Input
type='number'
name='price'
id='price'
required
className='pl-6'
/>
{state?.errors?.hours && (
<p className='py-2 text-xs text-red-500'>
{state.errors.price}
</p>
)}
</div>
</div>
<div className='mb-4 flex flex-col'>
<div className='flex items-center'>
<Switch
checked={customEndDate}
className='mr-2'
onCheckedChange={handleSwitchChange}
/>
<span className=''>Set custom end date</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<InfoCircledIcon className='ml-1' />
</TooltipTrigger>
<TooltipContent>
<p>By default cards are valid for one year</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{customEndDate ? (
<>
<Label htmlFor='ends_at' className='my-2 mr-2'>
Valid until
</Label>
<input
aria-label='Date'
type='date'
id='ends_at'
name='ends_at'
required
defaultValue={formattedDate}
/>
</>
) : (
<input
type='hidden'
aria-label='Date'
id='ends_at'
name='ends_at'
required
defaultValue={formattedDate}
/>
)}
</div>
<p aria-live='polite' className='sr-only'>
{state?.message}
</p>
<div className='mb-4'>
{errorMessage && (
<p className='py-2 text-xs text-red-500'>{errorMessage}</p>
)}
</div>
<DialogClose asChild>
<Button variant='outline' className='mr-2'>
Cancel
</Button>
</DialogClose>
<SubmitButton normal='Add card' going='Adding card...' />
</form>
)}
</p>
</DialogContent>
</Dialog>
</>
)
}
export default CreateCardDialog
It has a horrible useEffect but that was the only way if
- you click add card, you fill in the form wrong and it shows an error message
- you click cancel cause you dont wanna fill in the form anymore
- you change your mind and open the dialog again it kept showing the error still.
- that was solved by that horrendous useEffect 🙂 any ideas are welcome.
Now the problem that I am having is that after submitting the form successfully I cannot click add card again. It does not open again. Probably also something with resetting ..
The code is on Github (/app/u/cards/…)
https://github.com/santivdt/punchcards
It is also deployed here: https://punchcards.vercel.app/login where you can login with demo credentials to experience it firsthand :).
I did find this tutorial https://www.robinwieruch.de/next-forms/ but i am learning and struggle to understand what is going on.
Maybe someone has an idea?
Thanks in advance.
Ps. when reading through the code here I think it might have to do with double nesting children? From Header to the Dialog? Maybe not ..