IdoPesok / zsa

https://zsa.vercel.app
MIT License
761 stars 23 forks source link

useServerAction and useTransition #zsa-react #156

Closed floatrx closed 3 months ago

floatrx commented 3 months ago

Issue: execute async function never fulfilled when it invoked inside startTransition cb.

In this example I log all statuses into console. When i invoked execute fn inside startTransition cb isPending (from useTransition) always true and loader never ending...

~console.log:
status idle
START
status pending
status pending

but when i replacing execute with executeFormAction || or use action directly – everything works great...

~console.log:
status idle
START
status pending
status pending
status success
SUCCESS
FINISHED

I need useTransition to handle redirecting process router.push(/blog/${post.slug});...

// Example 
export const EditPostForm: RC<IProps> = (props) => {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();
  const { execute, status } = useServerAction(updatePost, {
    onStart: () => {
      console.log('START');
    },
    onSuccess: () => {
      console.log('SUCCESS');
    },
    onError: (error) => {
      console.log('ERROR', error);
    },
    onFinish: () => {
      console.log('FINISHED');
    },
  });

  console.log('status', status);

  return (
    <PostForm
      loading={isPending}
      onSubmit={async (values) => {
        startTransition(async () => {
          const [post] = await execute({ id, values });
          if (!post) return;
          router.push(`/blog/${post.slug}`);
        });
      }}
    />
  );
};
IdoPesok commented 3 months ago

Hi, execute from useServerAction is already wrapped in a startTransition. So this is like a double transition which I think is breaking it.

TLDR: you don't need startTransition and can use the isPending from useServerAction .

export const EditPostForm: RC<IProps> = (props) => {
  const router = useRouter();
  const { execute, status, isPending } = useServerAction(updatePost, {
    onStart: () => {
      console.log('START');
    },
    onSuccess: () => {
      console.log('SUCCESS');
    },
    onError: (error) => {
      console.log('ERROR', error);
    },
    onFinish: () => {
      console.log('FINISHED');
    },
  });

  console.log('status', status);

  return (
    <PostForm
      loading={isPending}
      onSubmit={async (values) => {
        execute({ id, values });
      }}
    />
  );
};

Then I would move router.push(/blog/${post.slug}) to a redirect(/blog/${post.slug}) in your server action so isPending is true until redirect completes.

I have been thinking about introducing an execute without transition but haven't done it yet.

floatrx commented 3 months ago

Thnx! Understood, but in my case, this solution does not work correctly when a transition redirection is needed.

  const router = useRouter();
  const [isPending, startTransition] = useTransition();

  <PostForm
    loading={isPending}
    onSubmit={async (values) => {
      startTransition(async () => {
        const [post] = await updatePost({ id, values });
        if (!post) return;
        router.push(`/blog/${post.slug}`); // ← show loader until redirect is done...
      });
    }}
  />

thnx, again...

IdoPesok commented 3 months ago

Hi, happy to help make this work for you. Can you try instead of router.push, using the redirect method inside your server actions? I think that should work as a transition redirect. Once you do that, you can get rid of your startTransition. Execute already has a transition.

https://nextjs.org/docs/app/api-reference/functions/redirect

floatrx commented 3 months ago

Thank you for the advice, but a redirect will not be the default behavior in all cases.