Forms and data submission are crucial aspects of web applications. Next.js 13 introduces an innovative approach to handle form submissions and data mutations through Server Actions. In this three-part series, we will explore the power of Next.js 13's App Router in handling forms, form submissions, and error handling.
Next.js 13 introduces the concept of Server Actions, which simplifies form handling by allowing you to define asynchronous server functions directly within your components. Server Actions can be defined in both Server Components and Client Components, offering flexibility and progressive enhancement.
To enable Server Actions in your Next.js project, modify your next.config.js file as follows:
1 module.exports = { 2 experimental: { 3 serverActions: true, 4 }, 5 } 6
Here are some key points to understand about Server Actions:
Server Actions in Next.js 13 integrate seamlessly with the caching and revalidation architecture. When a form is submitted, the Server Action can update cached data and trigger revalidation for relevant cache keys.
Unlike traditional applications that limit you to one form per route, Server Actions allow multiple actions per route. The browser doesn't need to refresh on form submission. Next.js can efficiently return both the updated UI and refreshed data in a single network roundtrip.
Let's delve deeper into these concepts with some practical examples.
To create a server-only form, define the Server Action within a Server Component. You can define the action inline with the "use server" directive at the top of the function or in a separate file with the directive at the top of the file.
Here's a basic example:
1 export default function Page() { 2 async function create(formData: FormData) { 3 'use server' // Mutate data and revalidate cache 4 } 5 6 return <form action={create}>...</form> 7 } 8
In this example, we use the FormData data type, and the submitted data is accessible in the server action create.
Server Actions allow you to invalidate the Next.js Cache on demand. You can invalidate an entire route segment with revalidatePath:
1 'use server'; 2 import { revalidatePath } from 'next/cache'; 3 4 export default async function submit() { 5 await submitForm(); 6 revalidatePath('/'); 7 } 8
Or, you can invalidate a specific data fetch with a cache tag using revalidateTag:
If you want to redirect the user to a different route after a Server Action is completed, you can use the redirect function along with an absolute or relative URL:
1 'use server'; 2 import { redirect } from 'next/navigation'; 3 import { revalidateTag } from 'next/cache'; 4 5 export default async function submit() { 6 const id = await addPost(); 7 revalidateTag('posts'); // Update cached posts 8 redirect(`/post/${id}`); // Navigate to a new route 9 } 10
For basic form validation, HTML attributes like required and type="email" are recommended. However, for more advanced server-side validation, consider using a schema validation library like zod:
1 import { z } from 'zod'; 2 3 const schema = z.object({ 4 // Define your validation rules here... 5 }); 6 7 export default async function submit(formData: FormData) { 8 const parsed = schema.parse({ 9 id: formData.get('id'), 10 // Define the form data fields... 11 }); 12 13 // Handle validation and data processing... 14 } 15
You can utilize the useFormStatus hook to display a loading state while a form is submitting on the client side:
1 'use client'; 2 import { experimental_useFormStatus as useFormStatus } from 'react-dom'; 3 4 function SubmitButton() { 5 const { pending } = useFormStatus(); 6 7 return ( 8 <button disabled={pending}> 9 {pending ? 'Submitting...' : 'Submit'} 10 </button> 11 ); 12 } 13
In this case, displaying loading or error states currently requires using Client Components. Future updates may provide server-side options for retrieving these values.
Server Actions in Next.js 13 can handle errors gracefully. They are capable of returning serializable objects, making it easier to manage both success and error messages. Here's an example of how you can handle errors in a Server Action:
1 'use server'; 2 3 export async function create(formData: FormData) { 4 try { 5 await createItem(formData.get('item')); 6 revalidatePath('/'); 7 return { message: 'Success!' }; 8 } catch (e) { 9 return { message: 'There was an error.' }; 10 } 11 } 12
In this scenario, the create function attempts to create an item based on the form data. If an error occurs, it returns an error message; otherwise, it returns a success message.
From a Client Component, you can read this value and save it to the state, allowing the component to display the result of the Server Action to the user:
1 'use client'; 2 import { create } from './actions'; 3 import { useState } from 'react'; 4 5 export default function Page() { 6 const [message, setMessage] = useState<string>(''); 7 8 async function onCreate(formData: FormData) { 9 const res = await create(formData); 10 setMessage(res.message); 11 } 12 13 return ( 14 <form action={onCreate}> 15 <input type="text" name="item" /> 16 <button type="submit">Add</button> 17 <p>{message}</p> 18 </form> 19 ); 20 } 21
Remember that displaying loading or error states currently requires using Client Components, but Next.js is actively exploring options for server-side functions to retrieve these values.
Optimistic updates allow you to update the user interface optimistically before the Server Action finishes, rather than waiting for the response. This can provide a smoother user experience. Here's how you can implement optimistic updates in Next.js 13:
1 'use client'; 2 import { experimental_useOptimistic as useOptimistic } from 'react'; 3 import { send } from './actions'; 4 5 type Message = { 6 message: string; 7 }; 8 9 export function Thread({ messages }: { messages: Message[] }) { 10 const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>( 11 messages, 12 (state: Message[], newMessage: string) => [ 13 ...state, 14 { message: newMessage }, 15 ] 16 ); 17 18 return ( 19 <div> 20 {optimisticMessages.map((m) => ( 21 <div>{m.message}</div> 22 ))} 23 <form 24 action={async (formData: FormData) => { 25 const message = formData.get('message'); 26 addOptimisticMessage(message); 27 await send(message); 28 }} 29 > 30 <input type="text" name="message" /> 31 <button type="submit">Send</button> 32 </form> 33 </div> 34 ); 35 } 36
In this example, the UI is updated optimistically by adding the new message to the list of messages even before the send action is complete.
Next.js 13's Server Actions also allow you to set cookies within the action using the cookies function:
1 'use server'; 2 import { cookies } from 'next/headers'; 3 4 export async function create() { 5 const cart = await createCart(); 6 cookies().set('cartId', cart.id); 7 } 8
Here, after creating a cart, we set a cookie named 'cartId' with the cart's ID value.
Reading cookies within a Server Action is straightforward using the cookies function:
1 'use server'; 2 import { cookies } from 'next/headers'; 3 4 export async function read() { 5 const auth = cookies().get('authorization')?.value; 6 // ... Continue with your logic 7 } 8
In this example, we retrieve the 'authorization' cookie value.
To delete cookies from within a Server Action, you can use the cookies function:
1 'use server'; 2 import { cookies } from 'next/headers'; 3 4 export async function delete() { 5 cookies().delete('name'); 6 // ... Continue with your logic 7 } 8
Deleting cookies is essential for maintaining user privacy and security.
Next.js 13's Server Actions provide flexibility for advanced form customization. You can extend this customization by modifying form behavior, layout, and appearance according to your application's requirements.
For instance, you can add advanced CSS styling, including properties like border-radius, font-family, and font-size to enhance the form's visual appeal:
1 <style jsx>{` 2 form { 3 display: flex; 4 flex-direction: column; 5 align-items: center; 6 } 7 input[type="text"] { 8 border: 1px solid #ccc; 9 border-radius: 4px; 10 font-family: 'Arial', sans-serif; 11 font-size: 16px; 12 padding: 8px; 13 margin: 4px; 14 } 15 button[type="submit"] { 16 background-color: #0070f3; 17 color: white; 18 border: none; 19 border-radius: 5px; 20 font-family: 'Helvetica', sans-serif; 21 font-size: 18px; 22 padding: 10px 20px; 23 margin-top: 16px; 24 cursor: pointer; 25 } 26 `}</style> 27 28
This CSS code snippet demonstrates how to apply styles to form elements such as text inputs and submit buttons. You can further customize styles to match your application's design.
Next.js 13's Server Actions are not limited to simple forms with basic input fields. You can efficiently handle complex data structures, including objects and arrays, in your forms.
For example, suppose you have a form that collects data for creating a new user account, including personal information and preferences. You can structure your form and Server Action to handle this complex data:
1 // Define the structure of user data 2 const userSchema = z.object({ 3 firstName: z.string(), 4 lastName: z.string(), 5 email: z.string().email(), 6 preferences: z.object({ 7 darkMode: z.boolean(), 8 newsletter: z.boolean(), 9 }), 10 }); 11 12 // Server Action for creating a user 13 export async function createUser(formData: FormData) { 14 const userData = userSchema.parse({ 15 firstName: formData.get('firstName'), 16 lastName: formData.get('lastName'), 17 email: formData.get('email'), 18 preferences: { 19 darkMode: formData.get('darkMode') === 'on', // Convert checkbox value to a boolean 20 newsletter: formData.get('newsletter') === 'on', // Convert checkbox value to a boolean 21 }, 22 }); 23 24 // Process and save the user data 25 // ... 26 } 27
In this example, we use the zod library to define the structure of user data. The Server Action createUser extracts and validates form data, including complex preferences, before further processing.
To enhance the user experience of your forms, you can incorporate features like client-side validation, real-time feedback, and dynamic form elements. Next.js 13's App Router provides the flexibility to implement these features seamlessly.
For instance, you can validate user inputs on the client side using JavaScript. Here's a simplified example of validating an email address:
1 // Client-side email validation 2 function isEmailValid(email) { 3 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 4 return emailRegex.test(email); 5 } 6 7 export default function Page() { 8 const [email, setEmail] = useState(''); 9 const [isValidEmail, setIsValidEmail] = useState(true); 10 11 function handleEmailChange(event) { 12 const newEmail = event.target.value; 13 setEmail(newEmail); 14 setIsValidEmail(isEmailValid(newEmail)); 15 } 16 17 return ( 18 <form> 19 <input 20 type="email" 21 value={email} 22 onChange={handleEmailChange} 23 className={isValidEmail ? '' : 'invalid'} 24 /> 25 <button type="submit">Submit</button> 26 </form> 27 ); 28 } 29
In this example, the handleEmailChange function checks if the entered email is valid and updates the component's state accordingly, allowing you to apply styling to indicate the input's validity.
Next.js Forms are a fundamental part of building interactive web applications. They enable users to input and submit data seamlessly. Whether it's a basic contact form on your website's contact page or a comprehensive multi-step data entry process, Next.js Forms provide the structure and tools you need.
In the world of web development, data is king. Form Data refers to the information users input into a form. This data could include anything from a user's name and email address to more complex structured data like preferences or survey responses.
Form Submission is the process of sending the form data from the user's browser to your server for processing. During this process, it's crucial to handle potential errors gracefully. Error Messages are messages displayed to the user when something goes wrong during form submission, such as incomplete fields or invalid data.
In scenarios where you expect a high volume of submissions, it's essential to ensure your form can handle multiple submissions simultaneously. Managing these Form Submissions efficiently can prevent performance bottlenecks.
A Contact Page is a common use case for forms on a website. In Next.js, you can create an API Route to handle form submissions from your Contact Page. API Routes are serverless functions that can handle various tasks, including processing form data.
When a user submits a form, the data is sent to the server, and the server responds with a result. The submit action initiates this process. Once the server processes the data, it sends back a response that can include success or error messages.
API Keys are essential for securing your API routes. They serve as access tokens that verify the authenticity of incoming requests. Keeping your API Keys secure is crucial to protect your application from unauthorized access.
React is a popular JavaScript library for building user interfaces. Importing React into your project allows you to create reusable components, including form elements and UI elements.
API Routes provide the backbone for handling form submissions. These routes define the endpoints where data is sent and processed. You can create, configure, and secure API Routes for various aspects of your application.
The Form Endpoint is the specific URL where your form data is sent for processing. You can customize this endpoint to match your application's routing structure and naming conventions.
Label Input Fields appropriately to provide clear instructions to users. Labels improve the accessibility and usability of your forms. Proper use of the htmlFor attribute ensures that screen readers and assistive technologies can interpret the labels correctly.
Display Error Messages in a user-friendly manner to guide users when they make mistakes in their submissions. Well-crafted error messages help users understand and correct their errors efficiently.
Just as important as handling errors is communicating success. Success Messages let users know that their submissions were successful, reinforcing a positive user experience.
When designing a Contact Form, consider the user experience. A well-crafted Contact Form should be intuitive, easy to use, and guide users through the process of reaching out to you.
Environment Variables provide a secure way to manage sensitive configuration data, such as API Keys and secrets. These variables can be accessed within your application to ensure secure communication.
To start building your Next.js application, you can create a new project using the create-next-app command. This command sets up the development environment and project structure.
A Development Server allows you to work on your application locally before deploying it to a production environment. It provides a controlled environment for testing and development.
Post Requests are HTTP requests used to submit form data to a server. When handling form submissions, you need to manage the Form State, keeping track of user interactions and data changes.
Handling Errors gracefully is crucial for maintaining a positive user experience. Errors can occur at various stages of form submission, from data validation to server processing.
In a Next.js application, you may encounter both Client-Side Errors, which occur in the user's browser, and Server-Side Errors, which occur on the server while processing form submissions. Both types of errors need to be handled effectively.
Components are building blocks of a Next.js application. You can create reusable components to structure your forms and UI elements. Exporting components allows you to use them across different parts of your application.
Styling plays a vital role in the overall look and feel of your application. You can define styles for various components, ensuring that your forms and UI elements are visually appealing and user-friendly.
We've taken a deep dive into forms in Next.js 13's App Router, exploring Server Actions, data handling, error management, form customization, and more. Next.js 13's Server Actions empower you to create dynamic, interactive forms that enhance the user experience while simplifying data processing and validation.
As you continue to work with Next.js 13's App Router, remember to adapt these concepts to your specific application's needs. The flexibility and power of Server Actions enable you to create forms that not only capture user input but also provide a delightful and engaging experience.
Thank you for joining us on this journey into the world of forms in Next.js 13. We hope you found this series informative and inspiring as you develop your web applications. If you have any questions or require further assistance, please feel free to reach out.
Happy coding, and may your forms be both functional and user-friendly!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.