In this tutorial we want to explore how to show toast notifications for Server Actions in React when using Reactâs useActionState Hook in combination with useEffect.
Feel free to check out the whole implementation in the GitHub repository .
In preparation for displaying toast notifications, we need to adjust our Server Action to return a response object that includes an additional timestamp which will allow us to execute callback functions in Reactâs useEffect Hook later:
We also need to adjust the type definition for the Server Actionâs response object:
Unfortunately, including the timestamp is necessary to prevent displaying outdated toast messages. We only want to show the toast messages once and not with every re-render of the component.
For the sake of completeness, the Server Action that we created in the previous tutorial:
To manage Server Actionâs state, we use Reactâs useActionState Hook . This hook allows us in a Client Component to transfer state between the client and server with a Server Action. We pass the Server Action function and an initial empty state object to the hook:
Do not forget the imports at the top of the file:
We also use a handcrafted Form component which will allow us later to implement the toast notification by listening to the changing state of the Server Action. We encapsulate this in a Form component to make it reusable for other components later.
Not all props are used yet, but we will use them later for different use cases.
When you interact with the button, there is no toast message yet because we will have to implement this toast feedback in the form button component. But the Server Action is already triggered and the userâs upvotes are increased.
Now that we have a button triggering the Server Action, we need to display a toast notification when the action completes. The simplest approach is to use Reactâs useEffect Hook inside the form button component to listen for changes of the server Actionâs state and display the message with the toast function:
However, simply displaying the toast inside useEffect is not reliable. If the component re-renders, it might show an old toast message even if no new action was triggered. To prevent duplicate toasts, we use the timestamp to track when the state was last updated:
The timestamp serves as a crucial reference for tracking changes in the action state. By comparing the current timestamp with the previous one, we can determine if the action state has changed. If thereâs a change, we display the new toast message.
Next we are going to include the status of the toast notification by either showing a success or error toast message depending on the status of the action state:
Last but not least, we will show a pending toast message when the action is still pending:
This setup already represents a minimal implementation of toasts: the Server Action returns both a message and a timestamp and the Client Component is responsible for displaying this information. In this minimal scenario, the FormButton abstraction wouldnât even be necessary, but it will be a good practice when there are more components that need to trigger Server Actions in the future.
To demonstrate how to handle errors, weâll add a downvote Server Action that always returns an error message. This will allow us to test how our app handles failed Server Actions and displays appropriate toast notifications.
Next, we add a new button in the User component that calls the new Server Action. When the action fails, it should trigger a toast notification informing the user that something went wrong:
Do not forget the import for the new Server Action:
Now, when you click the new button, you should see a toast message displaying the failed Server Action message.
To test another real-world scenario, weâll add a delete Server Action that removes a user. However, deleting a user introduces an interesting issue related to toast feedback.
And next we implement the Server Action to delete a user:
The Server Action works as expected, but the success toast message is not displayed because the page refreshes immediately after the Server Action completes. As a result, we never have the chance to compute the returned action state, since the component is unmounted due to the page refresh. This is a classic case of a race condition, where the componentâs lifecycle is interrupted before the state can be processed.
This is an important reason why I suggest to go with the alternative approach , which handles the toast message independently from the page refreshes and component unmounts.
To work around this issue, I implemented a solution where the page is refreshed on the client-side, rather than on the server-side, after the Server Action is performed. This ensures that the toast message is displayed before the refresh occurs.
First, we remove the server-side cache invalidation from the delete Server Action:
Second, we add a client-side application-wide cache refresh to the component:
Do not forget the import for the client-side router:
The FormButton component already accepts an onSuccess prop, but it hasnât been used yet. Letâs update the component to call this prop optionally whenever itâs provided. This way, if the onSuccess function is passed to the component, it will be executed after a successful action:
Now, we are performing a full client-side cache invalidation after the Server Action, which isnât ideal. However, this workaround allows us to display the toast message in Next.js for components that are unmounting after the Server Action. A better solution would be to trigger the toast message alongside the server-side cache invalidation, ensuring a more seamless user experience. Unfortunately, this doesnât seem possible with the current setup.
The example has shown you how to listen reactively to Server Actions in React and display toast notifications accordingly. Weâve also referenced the alternative approach which handles toast messages independently from page refreshes and component unmounts.
In this tutorial, we learned how to implement React Server Actions with Toasts:
If you enjoyed this tutorial and want to dig deeper into this topic, feel free to check out the alternative approach which has been referenced in this tutorial.