I'm getting a weird behavior when using the useOptimistic hook from React.
Running:
"next": "^14.0.4" with app router.
When I click the button I get 2 console log outputs instantly.
In the first log I can see the optimistic result:
{
"__typename": "GuideBlocksFormWebsite",
"id": "1",
"value": true,
"pending": true
}
And then instantly directly after it's reverted to the init state as seen in the second log (not the desired state as the value should be updated to true):
{
"__typename": "GuideBlocksFormWebsite",
"id": "1",
"value": false
}
SSRPage:
import { cookies } from 'next/headers'
import { createClient } from '@/utils/supabase/server'
import { Form } from './Form'
export const dynamic = 'force-dynamic'
const SSRPage = async () => {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { data: form } = await supabase.from('forms_progress').select()
return (
<>
<Form initFormData={form} />
</>
)
}
export default SSRPage
initFormData looks like this:
form: [
{
id: '97b1672d-b119-473e-958c-8d1c0902fc09',
}
]
Form:
'use client'
import { useOptimistic, useState } from 'react'
import TickButton from './TickButton'
const keysToRender = ['a', 'b', 'c', 'd', 'e', 'f']
export const Form = ({ initFormData }) => {
const [form, setForm] = useState(initFormData[0])
const [optimisticTick, addOptimisticTick] = useOptimistic(form, (state, newValue) => {
return {
...state,
[newValue.key]: newValue.value,
}
})
const renderButtons = () => {
return keysToRender.map((key) => (
<TickButton
key={key}
id={key}
value={optimisticTick[key]}
addOptimisticTick={addOptimisticTick}
/>
))
}
return <>{optimisticTick && renderButtons()}</>
}
TickButton:
'use client'
import { useTransition } from 'react'
import useGuideForm from '@/app/hooks/useGuideForm'
import { useRouter } from 'next/navigation'
const TickButton = ({ id, value, addOptimisticTick }) => {
const { updateFormsProgress } = useGuideForm()
const [, startTransition] = useTransition()
const router = useRouter()
const handleOnClick = async () => {
startTransition(() => {
addOptimisticTick({ key: id, value: true, pending: true })
})
await updateFormsProgress({
[id]: true,
})
router.refresh()
}
return (
<button type="button" onClick={handleOnClick} disabled={value === true}>
{value ? 'Ready' : 'Confirm'}
</button>
)
}
export default TickButton
updateFormsProgress:
const updateFormsProgress = async (data: unknown) => {
try {
if (user) {
const { error } = await supabase
.from('forms_progress')
.update(data)
.eq('id', user?.id as string)
if (error) {
throw new Error(error.message)
}
}
} catch (error) {
captureException(error)
}
}
Uploaded video here of the behavior: video
The flow:
Do you have any clues to what's going on here?
I encountered a similar issue while attempting to implement the useOptimistic hook without resorting to server actions, revalidateTag, and 'no-cache'. While I can't pinpoint why your code isn't functioning as expected, it appears as though it should. I'm not sure if it potentially could be some kind of caching issue.
However, a potential resolution could involve the following steps:
'use server'
import { revalidateTag } from 'next/cache'
export const mutateFormProgress = async (keyToUpdate, rowId) => {
await fetch(
`http://localhost:3000/api/form-progress?rowId=${rowId}`,
{
method: 'POST',
cache: 'no-cache',
body: JSON.stringify({
keytoUpdate,
}),
headers: {
'Content-type': 'application/json',
},
}
)
revalidateTag('form-progress')
}
const handleOnClick = async () => {
startTransition(() => {
addOptimisticTick({ key: id, value: true, pending: true })
})
await mutateFormProgress(keyToUpdate, rowId)
}
const response = await fetch(
`http://localhost:3000/api/form-progress}`,
{
cache: 'no-cache',
next: {
tags: ['form-progress'],
},
}
)
const formData = await response.json()
Additionally, it's essential to create an API endpoint, e.g., api/form-progress, with endpoints for both fetching (GET) and mutating (POST) data from the database table.