ProNextJS
    Loading
    lesson

    Next.js App Setup

    Jack HerringtonJack Herrington

    To start, run the create-next-app package using the npx command. We'll use the "--use-pnpm" option to ensure all dependencies get installed using the pnpm package manager.

    npx create-next-app@latest --use-pnpm
    

    When the prompt appears, we'll name the project app-router-forms. Say "yes" to TypeScript, ESLint, Tailwind CSS, the src directory, and the App Router. However, we'll say "no" to customizing the default import alias. This will keep the top level of the app clean.

    npx create-next-app@latest --use-pnpm
    
    What is your project named? ... app-router-forms
    
    Would you like to use TypeScript? ... Yes
    
    Would you like to use ESLint? ... Yes
    
    Would you like to use Tailwind CSS? ... Yes
    
    Would you like to use 'src/ directory? ... Yes
    
    Would you like to use App Router? (recommended) .. Yes
    
    Would you like to customize the default import alias (@/%)? > No
    

    The application setup will take a moment. Once it's done, open the project in your preferred code editor.

    Initializing shadcn

    Next we're going to initialize shadcn to make it ready for any components we want to bring in.

    Open a terminal, and with the npx command we'll install and initialize the latest version of shadcn-ui:

    npx shadcn-ui@latest init
    

    A prompt will appear asking for your style preferences. I'll choose the default style with Slate as the base color and CSS variables for colors.

    Which style would you like to use? ... Default
    Which color would you like to use as base? ... Slate
    Would you like to use CSS variables for colors? ... Yes
    

    After initialization, a components.json file will be created in the root directory. This file tells the shadcn command where everything in our project is and what our style preferences are:

    // components.json
    {
      "$schema": "https://ui.shadcn.com/schema.json",
      "style": "default",
      "rsc": true,
      "tsx": true,
      "tailwind": {
        "config": "tailwind.config.ts",
        "css": "src/app/globals.css",
        "baseColor": "slate",
        "cssVariables": true,
        "prefix": ""
      },
      "aliases": {
        "components": "@/components",
        "utils": "@/lib/utils"
      }
    }
    

    The shadcn initialization also makes changes to the tailwind.config.ts file to add CSS variables and defines those CSS variables in the src/app/globals.css file with the actual colors.

    Adding Our First Components

    With shadcn set up, we can now add our first components.

    The form system is the most critical component we need to add.

    Use the npx shadcn@latest command again, but this time with the add option to specify the component system to be added. In this case, we'll add the form system:

    npx shadcn-ui@latest add form
    

    Form brings in a bunch of components out of the box, including button, form, and label, which can be found over in the components UI directory. Note that these files are editable allowing for customization according to your project needs, which isn't always the case with other UI libraries.

    Along with the form component, shadcn automatically adds new dependencies for us. The react-hook-form library for form management and @hookform/resolvers to connect the Zod library for schema validation.

    In addition to the form system, we're going to need an input component. Use the same command as before to bring in the input field:

    npx shadcn-ui@latest add input
    

    Once that's done, it's time to start the application in development mode using pnpm:

    pnpm dev
    

    Editing the Starter Code

    Once the application is up and running, we'll make a few changes to the starter code.

    The first thing we'll do is change the body to dark mode. Inside of src/app/globals.css we'll add the following to apply dark mode to the body:

    // inside src/app/globals.css
    body {
      @apply dark;
    }
    

    The page will switch to dark mode, but there will still be boilerplate code to remove.

    Inside of src/app/page.tsx we can clear out everything in the Home component and replace it with an empty div:

    // inside src/pages/index.tsx
    export default function Home() {
      return <div></div>;
    }
    

    Now we have a blank slate to start building our form!

    Building a User Registration Form

    Our user registration form will include first name, last name, and an email field.

    To build this, the first step is creating a schema to validate user input against.

    Create a new file called registrationSchema.tsx inside of the src/app directory. At the top of the file, we'll import z from the Zod library, then we'll export a schema for validation by using Zod's object.

    import { z } from 'zod';
    
    export const schema = z.object({
    
    });
    

    The first item we'll add to the schema will be called first for the first name. This will be a string type that we'll trim and validate to ensure it's at least one character long. If the input doesn't meet this requirement, we'll return a message that the first name is required. We can do the same for the last field:

    export const schema = z.object({
      first: z.string().trim().min(1, {
        message: 'First name is required'
      }),
      last: z.string().trim().min(1, {
        message: 'Last name is required'
      }),
    });
    

    The next field we'll add to the schema is email. This will be a string type that we'll validate as an email address. If the input doesn't meet this requirement, we'll return a message that the email address is invalid:

    export const schema = z.object({
      first: z.string().trim().min(1, {
        message: 'First name is required'
      }),
      last: z.string().trim().min(1, {
        message: 'Last name is required'
      }),
      email: z.string().email({
        message: 'Invalid email address'
      }),
    });
    

    Now that the schema has been defined, we can move on to creating the registration form.

    Creating the Registration Form

    Create a new file at src/app/RegistrationForm.tsx and import the useForm hook from react-hook-form. The export will be the RegistrationForm component:

    // inside of src/app/RegistrationForm.tsx
    "use client";
    
    import { useForm } from 'react-hook-form';
    
    export const RegistrationForm = () => {
    
    }
    

    The useForm hook can take a lot of different parameters, but the one you'll use most often is defaultValues. In this case, we'll set the first, last, and email fields to an empty string, which matches the structure of our schema:

    export const RegistrationForm = () => {
      const form = useForm({
        defaultValues: {
          first: '',
          last: '',
          email: '',
        }
      });
    }
    

    Connecting the Form to the Schema

    Once we've added the default values, we can connect the form to our schema. To do this, we'll import the schema from registrationSchema.tsx and the zodResolver from @hookform/resolvers/zod.

    import { zodResolver } from '@hookform/resolvers/zod';
    import { schema } from './registrationSchema';
    

    We'll then use the template syntax after useForm to specify the schema to the form:

    export const RegistrationForm = () => {
      const form = useForm<{
        first: string;
        last: string;
        email: string;
      }>({
        resolver: zodResolver(schema),
        defaultValues: {
          first: "",
          last: "",
          email: "",
        },
      });
    };
    

    Now our form is connected to the schema and ready to be built out with the appropriate fields. However, this is kind of a lot of typing.

    We can simplify this by using the useForm hook to infer the type from the schema using Zod's infer utility method. This will allow the form to know the structure of our schema and build out the JSX components accordingly.

    Inferring the Form Structure

    Start by importing z from Zod, then we'll create a new type called OurSchema that uses Zod's infer utility method to infer the type of our imported schema:

    import { z } from 'zod';
    
    type OurSchema = z.infer<typeof schema>;
    

    Hovering over OurSchema shows us the inferred types we expect:

    // hover over OurSchema
    type OurSchema = {
        first: string;
        last: string;
        email: string;
    }
    

    Now that we know that z.infer works, instead of having the resolver option manually specified in useForm we can either use the zodResolver with the OurSchema type or just infer it directly:

    export const RegistrationForm = () => {
      const form = useForm<z.infer<typeof schema>>({
        resolver: zodResolver(schema),
        defaultValues: {
          first: "",
          last: "",
          email: "",
        }
      });
    };
    

    With the form connected to the schema, we can now build out the JSX for our form component.

    Building Out JSX for the Form Component

    The first thing to do is import the Button, Input, and Form related components from their respective locations:

    import { Button } from "@/components/ui/button";
    import { Input } from "@/components/ui/input";
    import {
      Form,
      FormControl,
      FormDescription,
      FormField,
      FormItem,
      FormLabel,
      FormMessage,
    } from "@/components/ui/form";
    

    The shadcn docs have excellent documentation for working with React Hook Form on the client side. We'll look more at server-side validation later.

    Inside the RegistrationForm component we'll return a Form component that will get the all of the output from the form we set up with useForm.

    // inside RegistrationForm
    
    return (
      <Form {...form}>
        <form className="space-y-8">
        </form>
      </Form>
    )
    

    Inside the Form component we'll add an email field and a Submit button. The email field will have a control of form.control, and the name will be set to email. Here's the basic structure for now:

    // inside RegistrationForm
    
    return (
      <Form {...form}>
        <form className="space-y-8">
          <FormField control={form.control} name="email">
          <Button type="submit">Submit</Button>
        </form>
      </Form>
    )
    

    Next we need to add a render function to the FormField component. The render function allows us to specify how we want the field to be laid out and the components that we will use. In this case, we'll use a FormItem along with a FormLabel, then a FormControl that contains the Input. The Input takes care of its value, onChange, onBlur, etc. We'll also add a FormDescription and FormMessage that will display any validation errors:

    return (
      <Form {...form}>
        <form className="space-y-8">
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input placeholder="" {...field} />
                </FormControl>
                <FormDescription>Your email address.</FormDescription>
                <FormMessage /> *
              </FormItem>
            )}
          />
          <Button type="submit">Submit</Button>
        </form>
      </Form>
    );
    

    This is a good stopping point to check our progress.

    Checking Our Progress

    On the page, we can see that the Email form is being displayed:

    The email form on the page

    However, the form is currently taking up the entire width of the screen. We can constrain this to make it smaller and more visually appealing by setting the maximum width of the Home component div to max-w-xl:

    // inside src/pages/index.tsx
    export default function Home() {
      return (
        <div className="mx-auto max-w-xl">
          <RegistrationForm />
        </div>
      );
    }
    

    Now the form looks better on our page:

    The more visually appealing form

    Add the Remaining Fields

    At this point, we have successfully set up an email field.

    Your task is to add the first and last name fields to the form. You'll see how I did this in the next section!

    Transcript

    All right, so here's how I do it. I'm going to use the mpx command to run the create-next-app package at the latest version to get the latest code. And then I'm going to use this use-pmpm option so that it uses pmpm as the package manager as opposed to npm. I'm a pmpm fan, and this will just make sure

    that all the dependencies are installed using pmpm. So I'm going to name my project AppRouterForms. You can name it whatever you want. I'm definitely going to use TypeScript. You can use ESLint if you want or not. I'm definitely going to choose Tailwind because ShadCN relies on Tailwind. I'm going to choose to use the source directory.

    That puts all of the source for the entire application in the source directory within the top level. So that keeps the top level of the app really clean. I'm definitely going to choose to use the AppRouter. It's recommended. And I'm not going to customize my import alias. This just means that @, if I use it in import,

    will be relative to the source directory. All right, I'm going to bring that up in VS Code. The next thing we need to do is initialize ShadCN. So I'm going to go to my console. And then I'm going to initialize ShadCN using the mpx command again, this time with the ShadCN UI package.

    And the init command, that's going to initialize the project so that it's ready to get any components we want to bring in from ShadCN. We get some options here, like the style that we want. I'm just going to pick the defaults. And then I'm going to say that I want to use CSS variables for colors. And we're initialized.

    Now let's actually go take a look at what's happened. So it's gone and created a components.json file in our root directory. That's basically for this mpx ShadCN command to know where everything is, what my style preferences are, and all that. It's gone and made some changes to the tailwind configuration file to add CSS variables.

    And then it's defined those CSS variables over in our globals.css, with the actual colors. Back at the top level, let's look at package.json. Now we can see it's already added some libraries, just to support the basic shell of ShadCN. So now let's go and add in our first components.

    Now the most critical one we want to add is the form system. So we're going to use mpx again, and then ShadCN UI. In this case, we're going to use add instead of init. We're already inited. And we're just going to give it the name of the component system we want, which in this case is form. Now I say component system, because form actually brings in a bunch

    of components out of the box. You can see your ShadCN components over in the components UI directory. In this case, it's brought in button, form, and label. Now the interesting thing about ShadCN is that you can actually edit these files. They're there to be edited and customized in your project.

    It's not like other UI libraries, where you can't actually edit the components. Here, they actually just copy it in your project, and you can make the changes. I'm not going to make any changes, but you can if you want to. Now it's interesting. Having added form, ShadCN automatically added some new dependencies

    for us, including React Quick Form to manage our forms, resolvers, which allow you to connect schema validation libraries like Zod or Joy or AGV to React Quick Form. In this case, it also brought in Zod, so we're going to use Zod to define our schema.

    Now the format we're going to build has only input controls, so you also need to bring in the input component. To do that, we use the exact same ShadCN command. We'll just bring in input. And there we go. We've got our input field. Now that our application is configured, let's bring it up in development mode.

    To do that, I use pnpm and then dev. It's got a stark white background. I want to work in dark mode, so the first thing I'm going to do is set the body to dark. To do that, I go to the app and then global CSS, and then add body, and then apply the tailwind for dark.

    That applies the dark mode theme. Let's go take a look in browser. And now everything's dark. Of course, I've got all this boilerplate in there, so let's get rid of all that. To do that, we just go into page.tsx. And then we remove everything and replace it with a div.

    We go back to our page, nice and blank and ready to go, ready for our form. So we're going to build a user registration form. It's going to have a first name, last name, and an email field. And the first thing we want to do is build a schema for that that we can validate any user input against.

    So let's go and create a registration schema in a file called registration schema.tsx. Then we'll bring in Zod. Zod's what we're going to use to define our schema. Z is just an object. It's got a lot of little helper methods in it

    that allow us to create a schema that we can then validate against. Now, our base object here is going to be an object. It's going to have keys for first, last, and email. So we use Z.object to define our object. And we export that as schema.

    So let's define our first field. We'll say that first is going to be a type of string. So we use Z.string. Now, there's two types of things that you can do with Zod. You can have it clean up the data. In this case, we want to trim it down. So let's use .trim. Now, we'll trim any white space off the left and right-hand sides

    of the string. And then we apply any validations we want. In this case, the validation we want to use is the minimum number of characters. We want to make sure that they've actually typed something in. So we say that we want at least one character. And if you fail that, then the message is that the first name is required.

    So let's do the same thing for last. Exactly the same thing, except last instead of first. And we massage the message a little bit. And then finally, we want an email field. So we do pretty much the same thing. But instead of min, we use email for our validation. Now, we don't need to define any extra parameters on email.

    In this case, we just need to say that the message is invalid email address if it turns out to be an invalid email address. Now that we've got that going, we actually want to create a registration form. That's going to be a client component. It's going to use a hook.

    In this case, that is the use form hook from React hook form. But before we do that, let's just create a simple registration form. And it'll bring in use form. And then we'll invoke that within our registration form. So use form can take a lot of different parameters. The one that you often use is the default values.

    So in this case, first, last, and email are going to be defaulted to an empty string. So now we want to connect that to our schema. Let's bring in our schema from the registration schema. Now, to connect those two, we need a resolver. So we're going to bring in the Zod resolver from Hook Form Resolvers. Hook Form Resolvers has resolvers

    for all kinds of schema validation libraries. There are a bunch of them-- Joy, YUP, AJV. You can go and check out the full list on the Hook Form Resolvers NPM package. Now, down to use form, we set the resolver to the output of Zod resolver with our Zod schema.

    Now, how do we know that first, last, and email are the right ones? We could just put in foo here. And that may or may not be right. So what we really want to do is tell use form what the structure of our schema is in TypeScript.

    To do that, we use a template syntax after use form. And now we see that use form has applied that schema to the default values. And so foo is invalid. That's cool, but really, we don't want to have to type all this. We want this just to come from the schema. So is there a way to infer that from our Zod schema

    and turn our Zod schema into TypeScript? Well, yes, there is. First thing we need to do is bring in Zod. And then we can create a new type called our schema, for example, by inferring using the z.infer utility type against the type of our schema.

    And what that comes out with is that exact same thing. We've got first name, last name, and email, and everything's in TypeScript. Awesome. So we can use that here in place of what we had before. Or we just want to keep it nice and terse.

    We can just put that in here instead of our schema. And again, foo is wrong, so let's get rid of that. Nice. Now our use form is configured to match the TypeScript schema of our registration form Zod schema. So let's start building out our form. So that brings in some UI components,

    including button and input. We need the button in order to submit, and we need the input in order to take input from the user. And then we're going to bring in the form components from ShadCN. That includes form. That's a wrapper around the form. Form control, which is a wrapper around individual controls

    within the form. And then a bunch of helper components like field, and item, and label, and so on and so forth. If you want to know more about this, there actually is excellent documentation on this. Over on the ShadCN site, let's go to form. So they got some excellent documentation on React Hook

    form and how it integrates with ShadCN. This is only client side. We're going to cover both client side and server side. In this tutorial. But there is an example section down below that you should have a look at, where they cover different types of controls and how to use them. For example, checkbox, date picker, radio group, and so on. Let's go back to our code, and we'll

    start building out our JSX for our component. So the first thing we want to do is bring in that form component, and then give it all of the output from our use form. That's going to give it all of the controls from use form, so that that component can then manage that form. But we do want a legit form tag inside of that.

    Let's add a Submit button, so we can submit that form. And then let's add the email field. To do that, we're going to add the form field component. To add the email field, we add form field. We give it the control that we get from form.control. That allows this form field component

    to manage this particular field. And then we give it the name of the field, which in this case is email. If I remove email, you can actually see that our pop-up hints to all of the available fields that we have. Don't bring back email. The last thing it needs is a render function. So render gets given field, and then you

    get to specify how you want that actual field to be laid out and all the components you're going to use. In my case, I'm going to use a form item. And then inside the form item, I'm going to have a form label that has the label. Then the form control that has our input. And we give that form control, in this case the input, all of the output of field.

    That's going to have the current value, on change, on blur, and all that good stuff. And then the description, which I'll just set to your email address. And then the message is going to have any validation messages. So I think this is good enough to actually see. Let's go back over to our page, and then bring this in,

    and then use it. Let's see. All right, looking pretty good. Although it does take up the entire width of the screen. So I'm going to constrain that a little bit. Just going to set the maximum width of this particular div to XL. That's going to make it small. And then MX Auto is going to bring it in and justify it on both sides.

    So let's hit Save and see how that looks. That's a lot more clean. OK, so now that we've got email going, it's up to you. You're going to go and add the first and last name fields to our form. You can check out how I did it in the next section.