skip to Main Content

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',
    'mrkoll.se': false,
    'eniro.se': false,
    'merinfo.se': false,
    'hitta.se': false,
    'ratsit.se': false,
    is_completed: false,
    created_at: '2023-12-05T23:26:54.592581+00:00',
    updated_at: '2023-12-05T23:26:54.592581+00:00',
    'birthday.se': false
  }
]

Form:

'use client'

import { useOptimistic, useState } from 'react'
import TickButton from './TickButton'

const keysToRender = ['mrkoll.se', 'eniro.se', 'merinfo.se', 'hitta.se', 'ratsit.se', 'birthday.se']

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:

  • Button has value false, the data is fetched during SSR and passed on to the useOptimistic hook as the init value.
  • Button is clicked the first time, and the optimistic value flickers.
  • Button returns to false value after flickering.
  • Button is clicked a second time, value is correctly set to true.

Do you have any clues to what’s going on here?

2

Answers


  1. you need to make sure you’re updating the original state that is being passed into your useOptimistic hook, not just calling the addOptimisticTick function…

    I think react is waiting for the original state (Tick) to update because thats why you would want to call useOptimistic, but the original state is not updating so it stops using the optimistic value and reverts to the old state.

    So you would need to call the update state function for the prop that is being passed into the component, useOptimistic isn’t a replacement for that prop value

    const [optimisticTick, addOptimisticTick] = useOptimistic(
        tick, 
        (prevTick, newTick) => {
            ...prevTick,
            ...newTick,
            pending: true
        }
    )
    
    const handleOnClick = (event) => {
        startTransition(() => {
          addOptimisticTick({value: true});
          // you need to update the original tick that was passed as the prop
         
          // Generally you make some sort of API call here
          // const tickResponse = await GENERIC_API_CALL(event);
          // tickResponse will be the updated tick so = {value: true}
    
          // setTick((prev) => { ...prev, ...tickResponse} // pending will go back to false or undefined but value will update
        });
    
        handleTickPage(event)
      }
    
    
    Login or Signup to reply.
  2. Try removing router.refresh() call in your handleOnClick function. I think this call might be causing the component to re-render, and since the data is fetched during SSR and passed on to the useOptimistic hook as the initial value, the state might be getting reset to its initial value.

    Try doing this instead:

    const handleOnClick = async () => {
      startTransition(() => {
        addOptimisticTick({ key: id, value: true, pending: true })
      })
    
      await updateFormsProgress({
        [id]: true,
      })
    
      // Fetch the new data
      const { data: newForm } = await supabase.from('forms_progress').select()
      
      // Update the state with the new data
      setForm(newForm[0])
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search