Django, InertiaJs and React (Vite) - A guide to get you started Pt. 2
Tags: django, inertiajs, react, vite
Published: 2023-01-28 17:47:19

:warning: The first part of this tutorial is based on `InertiaJs` prior to version `1.0`. Towards the end of this tutorial, we will tackle upgrading `InertiaJs`.

This is Part 2 of our Django, InertiaJs & React/Vite tutorial. If you have not looked at Part 1, I would recommend that you check it out [here](https://dev.to/saiforceone/django-inertiajs-and-react-a-guide-to-get-you-started-pt-1-1po).

:information_source: To make it easier for you to pick up where we left off or to make sure your project is in working order before we proceed, check out the repo [here](https://github.com/saiforceone/dj-intertiajs-vite-react-part1). Be sure to check the readme before continuing.

In this part of the tutorial series, we are going to be looking at a few things:

* Integrating TypeScript
* Installing Material UI (you can use any component library you want)
* Implementing the contact app
  * Backend 
    * models, views
  * Frontend
    * Pages and components
* Upgrading InertiaJs to 1.0

Let's get to it, shall we?

:warning: You may see an error running the project on (MacOS) after cloning the repo that looks like `zsh: permission denied: ./manage.py`, you will need to change the permission on that file by using chmod.

```shell
# Review the contents of any file that you are going to make executable before making it executable
chmod +x manage.py
```

The `manage.py` file should look like what is below

```python
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'contact_manager.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()
```



## Optional: Installing Prettier (It just makes sense)

Installing Prettier is optional, but still a pretty good idea :smirk:. So let's do that and set it up before proceeding.

* Install it using the command below

```shell
npm i -D prettier
```

* Create a prettier configuration file `.prettierrc.json` in the root of the project and copy and paste the content below (minimal prettier configuration)

```json
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}
```

* Depending on your editor or IDE, you may need to tell it to use Prettier.

## Integrating TypeScript

Now that we have a working project, let's make it even better with TypeScript. Adding TypeScript is going to make our lives easier in the long run, so let's add it now.

* Create a branch from main, we'll call it `part-2-ts-and-forms` or anything you want

```shell
git checkout -b part-2-integrating-ts-and-forms
```

* Install dependencies (as dev dependences) via npm

```shell
# install dev dependencies
npm i -D ts-loader typescript @types/react @types/react-dom
```

![dependencies installed](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777623/dev-tutorials/djir-contacts-p2/img-01_wxpgvm.png)

* Initialise TypeScript with some presets

```shell
npx tsc --init
```



![tsc init](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777623/dev-tutorials/djir-contacts-p2/img-02_zlhxkw.png)

* Replace the contents of the generated `tsconfig.json` file with what is shown below

```json
{
 "compilerOptions": {
   "target": "ESNext",
   "useDefineForClassFields": true,
   "lib": ["DOM", "DOM.Iterable", "ESNext"],
   "allowJs": false,
   "skipLibCheck": true,
   "esModuleInterop": false,
   "allowSyntheticDefaultImports": true,
   "strict": true,
   "forceConsistentCasingInFileNames": true,
   "module": "ESNext",
   "moduleResolution": "Node",
   "resolveJsonModule": true,
   "isolatedModules": true,
   "noEmit": true,
   "jsx": "react-jsx",
   "types": ["vite/client"],
 },
 "include": ["react-app/src"],
}
```



![package.json contents](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777668/dev-tutorials/djir-contacts-p2/img-03_bmlomt.png)

* Don't forget to add the `tsconfig.json` file to version control (if you're using git)
* Change the extensions of all our `.jsx` files to `.tsx` with the exception of `main.jsx`
* Rename `react-app/src/pages/Home/index.jsx` to end with `.tsx`
  * Modify our home component to add the necessary interface as shown below

```jsx
interface HomePageProps {
  contacts: string[];
}

export default function Home({ contacts }: HomePageProps) {
  return (
    <div>
      <h1>Contact List</h1>
      {Array.isArray(contacts) && contacts.length ? (
        <ul>
          {contacts.map((contact) => (
            <li key={contact}>{contact}</li>
          ))}
        </ul>
      ) : (
        <p>No contacts yet...</p>
      )}
    </div>
  );
}

```

:information_source: We will replace the contents of this file with something else eventually but, for right now, we don't want __TypeScript__ to complain

* Rename `react-app/src/components/Layout.jsx` to end with `.tsx`
  * Modify the layout component to add the necessary interface as shown below

```jsx
import React from 'react';
import type { FC } from 'react';

interface LayoutProps {
  children: React.ReactNode;
}

const Layout: FC<LayoutProps> = ({ children }) => {
  return (
    <main>
      <div>{children}</div>
    </main>
  );
};

export default (page: React.ReactNode | React.ReactElement) => (
  <Layout>{page}</Layout>
);

```

* Update our `main.jsx` component as follows

```javascript
// change this line
const pages = import.meta.glob('./pages/**/*.jsx');
// to
const pages = import.meta.glob('./pages/**/*.tsx');

// change this line
const page = (await pages[`./pages/${name}.jsx`]()).default;
// to
const page = (await pages[`./pages/${name}.tsx`]()).default;
```



![Main.jsx with changes](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777624/dev-tutorials/djir-contacts-p2/img-04_p5dkpy.png)

:information_source: We will be revisiting the converting of `main.jsx` to TypeScript in the upgrade to InertiaJs v1.0 section

* Re-run vite via `npm run dev` along with the Django dev server via `./manage.py runserver` and navigate to http://127.0.0.1:8000 and you shouldn't see any errors in the browser console

![Typescript components working without errors](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777623/dev-tutorials/djir-contacts-p2/img-05_lwoxha.png)

## Implementing the Contact Manager

We're now at the point where we can implement our contact manager application. Keep in mind that this is a somewhat-simplified example web application. We will be skipping authentication to keep this part short. (In a future Django/InertiaJs project, we'll build something more complicated complete with auth) We will need have a way to manage contacts and notes for each contact. Let's do some Django things.

* Models
  * Each model we create will have the these common fields
    * Created At
    * Updated At
  * `Model: Contact` - For each contact, we will want to keep track of the following items
    * First name
    * Last name
    * Middle Name
    * Email Address
    * Phone Number

* Create our contact app using django admin as follows

```shell
django-admin startapp contact
```

* If you're using git, go ahead and add all files from the `contact` app we just created to git
* Let's also add our newly created `contact` app to `INSTALLED_APPS` so that Django can 'see' it :eyes: in `settings.py`

```python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # django InertiaJS apps
    'django_vite',
    'inertia',
    # our apps
    'contact' # <- this line was added
]
```



* Let's define our models in our model file called...well, `models.py` located at `contact/models.py`

```python
import datetime

from django.db import models


# Create your models here.

class AppModel(models.Model):
    """
    We are using this as an abstract model so that we can define and reuse common fields across models
    """
    created_at = models.DateTimeField(default=datetime.datetime.utcnow)
    updated_at = models.DateTimeField(blank=True, null=True)

    class Meta:
        abstract = True


class Contact(AppModel):
    first_name = models.CharField(max_length=50)
    middle_name = models.CharField(blank=True, max_length=50, null=True)
    last_name = models.CharField(max_length=50)
    email_address = models.EmailField(max_length=200)
    phone_number = models.CharField(max_length=20)

    def __str__(self):
        return f'({self.id}) {self.first_name} {self.last_name}'


class ContactNote(AppModel):
    contact = models.ForeignKey('Contact', on_delete=models.CASCADE, related_name='ContactNoteOwner')
    content = models.TextField()

    def __str__(self):
        return f'({self.id}) Note for {self.contact.first_name} {self.contact.last_name}'
    
```

* With our models created, we can run our migration (this will get rid of the unapplied migrations warning)

```shell
# *nix or Mac OS
./manage.py makemigrations
./manage.py migrate
# windows
manage.py makemigrations
manage.py migrate
```



![Migrations created](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777623/dev-tutorials/djir-contacts-p2/img-06_blp3rf.png)

![Migrations applied](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777624/dev-tutorials/djir-contacts-p2/img-07_vcc1h0.png)

:information_source: While we have created a `ContactNote` model. I'll leave that up to you to make use of or I can do a Part 2.5 to this guide which covers that.

### Django Admin

Let's detour a bit and set up our Django admin so that we can easily create some test data for our application

* Create the django super user account by running the command below

```shell
./manage.py createsuperuser
```

* Fill out the information asked in the prompts. Once done, it should say that the superuser was created successfully.

![super user created](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777635/dev-tutorials/djir-contacts-p2/img-08_uk82z1.png):information_source: If you want to see all the commands available, you can just run `./manage.py` 

* Let's make sure things are working correctly by running the django dev server and navigating to http://127.0.0.1:8000/admin. You should see something like image below

![django admin login](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777633/dev-tutorials/djir-contacts-p2/img-09_ioerby.png)



![django admin logged in](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777632/dev-tutorials/djir-contacts-p2/img-10_jf3b8g.png)

* Let's register our models so that they also show up in the Django's admin interface by modifying the file `contact/admin.py`

```python
from django.contrib import admin
from contact import models # There are other ways to do the model imports

# Register your models here.


@admin.register(models.Contact)
class ContactAdmin(admin.ModelAdmin):
    pass


@admin.register(models.ContactNote)
class ContactNoteAdmin(admin.ModelAdmin):
    pass

```

* Reload the django admin interface and you should now see our models

![Django admin with models displayed](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777631/dev-tutorials/djir-contacts-p2/img-11_e4g8yd.png)

* Go ahead and create some contact and some notes via the admin interface and then we can get back to the fun stuff.
* Let's make a slight change to add a `nick_name` field to our contact model

```python
class Contact(AppModel):
    first_name = models.CharField(max_length=50)
    middle_name = models.CharField(blank=True, max_length=50, null=True)
    last_name = models.CharField(max_length=50)
    nickname = models.CharField(blank=True, max_length=50, null=True) # <- we added this
    email_address = models.EmailField(max_length=200, unique=True)
    phone_number = models.CharField(max_length=20, unique=True)

    def __str__(self):
        return f'({self.id}) {self.first_name} {self.last_name}'
```

* Let's make migrations and then apply them

```shell
./manage.py makemigrations
./manage.py migrate
```

![Migrations applied](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777634/dev-tutorials/djir-contacts-p2/img-13_nfefqi.png)

* Let's set up our model forms which we will use later for validation by creating the file  `contact/forms.py`

```python
from django.forms import ModelForm
from contact.models import Contact, ContactNote

# We are letting Django's model forms do the work for us. We are only scratching the surface of what can be done with Model forms.


class ContactForm(ModelForm):
    """
    Model form used for just validating our data since we're sending data via InertiaJS
    """
    class Meta:
        model = Contact
        exclude = ['created_at', 'updated_at']


class ContactNoteForm(ModelForm):
    """
    Model form used for just validating our data since we're sending data via InertiaJS
    """
    class Meta:
        model = ContactNote
        exclude = ['created_at', 'updated_at']

```



:information_source: By specifying `exclude`, we are telling the model form to use all fields but exclude the ones specified. For more information on Django Model Forms, click [here](https://docs.djangoproject.com/en/4.1/topics/forms/modelforms/).

* Let's work on a way to get our data from our database by creating a file called `dataSource.py`. We are keeping things simple here to keep the focus on `InertiaJs`.

```python
import datetime

from contact.models import Contact, ContactNote
from contact.forms import ContactForm, ContactNoteForm


def get_contact_summary():
    """
    Retrieves the count of contacts and contact notes
    """
    try:
        total_contacts = Contact.objects.count()
    except Exception as err:
        total_contacts = 0

    try:
        total_notes = ContactNote.objects.count()
    except Exception as err:
        total_notes = 0

    return {
        'totalContacts': total_contacts,
        'totalNotes': total_notes,
    }

# we'll add more to this later

```

* Let's make some changes to our `index` view in `contact_manager.views` so that we can have our contact summary displayed

```python
from inertia import inertia

from contact.dataSource import get_contact_summary


@inertia('Home/Index')
def index(request):
    return {
        'summary': get_contact_summary()
    }

```

* Next, we'll modify the corresponding component to show our summary and to make sure things are working. I opted to rewrite the component, but it still works the same way

```jsx
import { FC } from 'react';

interface HomePageProps {
  summary: {
    totalContacts: number;
    totalNotes: number;
  };
}

const HomeIndex: FC<HomePageProps> = ({ summary }) => {
  return (
    <div>
      <h1>Contact Summary</h1>
      <div>
        <div>
          <h2>{summary.totalContacts}</h2>
          <p>Total Contacts</p>
        </div>
        <div>
          <h2>{summary.totalNotes}</h2>
          <p>Total Contact Notes</p>
        </div>
      </div>
    </div>
  );
};

export default HomeIndex;

```

![Contact Summary Updated](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777643/dev-tutorials/djir-contacts-p2/img-14_glqaek.png)

Now would be a pretty good time to get some styling setup. We'll be using MUI React because we're efficient like that.

* Let's install Material UI

```shell
npm i -D @mui/material @emotion/react @emotion/styled @fontsource/roboto
```

![NPM package install](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777642/dev-tutorials/djir-contacts-p2/img-15_ykb375.png)

* Let's restructure our `Home/Index.tsx` component so it looks somewhat presentable
* We will create a card component so we can display information on the home page nicely `components/MetricCard.tsx`

```jsx
import React from 'react';
import type {FC} from 'react';
import { Card, CardActions, CardContent, Typography } from '@mui/material';

interface MetricCardProps {
  actions?: React.ReactElement;
  label: string;
  value: number | string;
}

export const MetricCard: FC<MetricCardProps> = ({ actions, label, value }) => {
  return (
    <Card variant="outlined">
      <CardContent>
        <Typography sx={{ fontSize: 80 }} variant="body1">
          {value}
        </Typography>
        <Typography sx={{ fontSize: 20 }} variant="h2">
          {label}
        </Typography>
      </CardContent>
      {actions && <CardActions>{actions}</CardActions>}
    </Card>
  );
};
```



* Let's update `Home/Index.tsx`

```jsx
import { FC } from 'react';
import { Inertia } from '@inertiajs/inertia';
import { Box, Button, Grid, Typography } from '@mui/material';
import { MetricCard } from '../../components/MetricCard';

interface HomePageProps {
  summary: {
    totalContacts: number;
    totalNotes: number;
  };
}

const HomeIndex: FC<HomePageProps> = ({ summary }) => {
  return (
    <Box sx={{ flexGrow: 1 }}>
      <Typography variant="h2">Contact Summary</Typography>
      <Typography variant="body1">
        Here's a summary of your contact list and notes
      </Typography>
      <Box sx={{ flexGrow: 1 }}>
        <Grid container direction="row" spacing={1}>
          <Grid item xs={12} md={6}>
            <MetricCard
              actions={
                <>
                  <Button variant="outlined">Add New Contact</Button>{' '}
                  <Button
                    onClick={() => Inertia.visit('/contact')}
                    variant="outlined"
                  >
                    View all Contacts
                  </Button>{' '}
                </>
              }
              label="Total Contacts"
              value={summary.totalContacts}
            />
          </Grid>
          <Grid item xs={12} md={6}>
            <MetricCard
              actions={
                <>
                  <Button variant="outlined">View all notes</Button>{' '}
                </>
              }
              label="Total Notes"
              value={summary.totalNotes}
            />
          </Grid>
        </Grid>
      </Box>
    </Box>
  );
};

export default HomeIndex;
```

* Let's add some icons via NPM

```shell
npm install -D @mui/icons-material
```

* Let's update our layout file `components/Layout.tsx`

```jsx
import React from 'react';
import type { FC } from 'react';
import { AppBar, Container, Toolbar, Typography } from '@mui/material';
import ContactsIcon from '@mui/icons-material/Contacts';

interface LayoutProps {
  // For our purposes, ReactNode will be fine.
  children: React.ReactNode;
}

const Layout: FC<LayoutProps> = ({ children }) => {
  return (
    <main>
      <AppBar position="static">
        <Container maxWidth="xl">
          <Toolbar disableGutters>
            <ContactsIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
            <Typography
              variant="h6"
              noWrap
              component="a"
              href="/"
              sx={{
                mr: 2,
                display: { xs: 'none', md: 'flex' },
                fontFamily: 'monospace',
                fontWeight: 700,
                letterSpacing: '.15rem',
                color: 'inherit',
                textDecoration: 'none',
              }}
            >
              Contact Manager - InertiaJs
            </Typography>
          </Toolbar>
        </Container>
      </AppBar>
      <div>{children}</div>
    </main>
  );
};

export default (page: React.ReactNode | React.ReactElement) => (
  <Layout>{page}</Layout>
);

```

![Updated layout](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777636/dev-tutorials/djir-contacts-p2/img-16_rr96rt.png)

Now we're getting somewhere. Let's press on :thumbsup:

* Let's work on our contact list functionality by editing the `contact/views.py`

```python
from django.shortcuts import redirect
from inertia import inertia

from contact import dataSource


@inertia('Contact/Listing')
def contacts(request):
    return dataSource.get_contacts()
```

* Because we're doing things the typescript way, let's create an interface based on `Contact` model in `react-app/src/interfaces/contact.interface.ts`

```typescript
export interface Contact {
  id: number;
  first_name: string;
  middle_name?: string;
  last_name: string;
  nickname?: string;
  email_address: string;
  phone_number: string;
}

```

* Let's create our contact listing `react-app/src/pages/Contact/Listing.tsx`

```jsx
import React from 'react';
import type { FC } from 'react';
import {
  Box,
  Divider,
  IconButton,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
} from '@mui/material';
import {
  DeleteForeverOutlined as DeleteIcon,
  EditRounded as EditIcon,
  InfoOutlined as InfoIcon,
  Person2Rounded as PersonIcon,
} from '@mui/icons-material';

import type { Contact } from '../../interfaces/contact.interface';

interface ListingProps {
  contacts: Contact[];
}

const Listing: FC<ListingProps> = ({ contacts }) => {
  return (
    <List sx={{ width: '100%' }}>
      {contacts.map((contact, index) => (
        <Box key={`item-${index}`}>
          {index ? <Divider /> : null}
          <ListItem
            key={`contact-${contact.id}`}
            alignItems="center"
            secondaryAction={
              <>
                <IconButton>
                  <InfoIcon />
                </IconButton>
                <IconButton>
                  <EditIcon />
                </IconButton>
                <IconButton>
                  <DeleteIcon />
                </IconButton>
              </>
            }
          >
            <ListItemIcon>
              <PersonIcon />
            </ListItemIcon>
            <ListItemText
              primary={`${contact.first_name} ${contact.last_name}`}
              secondary={contact.phone_number}
            />
          </ListItem>
        </Box>
      ))}
    </List>
  );
};

export default Listing;

```

![Listing layout](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777647/dev-tutorials/djir-contacts-p2/img-17_iehvel.png)

Not too shabby for our listing. Of course, you are free to go for maximum style points if you'd like

* Since we're looking at our listing, let's take this time to create a contact info modal component `react-app/src/components/ContactInfoDialog.tsx`

```jsx
import type { FC } from 'react';
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogTitle,
  Grid,
  ListItemText,
} from '@mui/material';
import { Close as CloseIcon, Edit as EditIcon } from '@mui/icons-material';

import type { Contact } from '../interfaces/contact.interface';

interface ContactInfoDialogProps {
  contact?: Contact;
  onClose: () => void;
  onEdit: () => void;
  open: boolean;
}

export const ContactInfoDialog: FC<ContactInfoDialogProps> = ({
  contact,
  onClose,
  onEdit,
  open,
}) => {
  return (
    <Dialog fullWidth maxWidth="sm" onClose={onClose} open={open}>
      <DialogTitle>Contact Information</DialogTitle>
      <DialogContent>
        <Grid container spacing={1}>
          <Grid item xs={4}>
            <ListItemText
              primary={contact?.first_name}
              secondary="First Name"
            />
          </Grid>
          <Grid item xs={4}>
            <ListItemText
              primary={contact?.middle_name || 'N/A'}
              secondary="Middle Name"
            />
          </Grid>
          <Grid item xs={4}>
            <ListItemText primary={contact?.last_name} secondary="Last Name" />
          </Grid>
          <Grid item xs={12}>
            <ListItemText
              primary={contact?.nickname || 'N/A'}
              secondary="Nickname / Alias"
            />
          </Grid>
          <Grid item xs={6}>
            <ListItemText
              primary={contact?.email_address}
              secondary="Email Address"
            />
          </Grid>
          <Grid item xs={6}>
            <ListItemText
              primary={contact?.phone_number}
              secondary="Phone Number"
            />
          </Grid>
        </Grid>
      </DialogContent>
      <DialogActions>
        <Button onClick={onEdit} startIcon={<EditIcon />}>
          Edit Contact
        </Button>
        <Button onClick={onClose} startIcon={<CloseIcon />}>
          Close
        </Button>
      </DialogActions>
    </Dialog>
  );
};
```

* We'll need to edit our listing `react-app/src/pages/Contact/Listing.tsx` so that we can use the newly created dialog

```jsx
// modified react import
import React, { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
import { Inertia } from '@inertiajs/inertia';
import {
  Box,
  Divider,
  Fab,
  IconButton,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
} from '@mui/material';
import {
  AddCircleOutlineRounded as AddIcon,
  DeleteForeverOutlined as DeleteIcon,
  EditRounded as EditIcon,
  InfoOutlined as InfoIcon,
  Person2Rounded as PersonIcon,
} from '@mui/icons-material';

import type { Contact } from '../../interfaces/contact.interface';
import { ContactInfoDialog } from '../../components/ContactInfoDialog';

interface ListingProps {
  contacts: Contact[];
}

const Listing: FC<ListingProps> = ({ contacts }) => {
  const [selectedContactId, setSelectedContactId] = useState<
    number | undefined
  >();

  // this was added
  const closeContactInfoModal = useCallback(() => {
    setSelectedContactId(undefined);
  }, []);

  // this was also added - we'll use this later
  const editContact = useCallback((contactId: number) => {
    Inertia.visit(`/contact/${contactId}`);
  }, []);

  const selectedContact: Contact | undefined = useMemo(() => {
    try {
      return contacts.find((contact) => contact.id === selectedContactId);
    } catch (e) {
      return undefined;
    }
  }, [contacts, selectedContactId]);

  return (
    <>
      <List sx={{ width: '100%' }}>
        {contacts.map((contact, index) => (
          <Box key={`item-${index}`}>
            {index ? <Divider /> : null}
            <ListItem
              key={`contact-${contact.id}`}
              alignItems="center"
              secondaryAction={
                <>
                  <IconButton onClick={() => setSelectedContactId(contact.id)}>
                    <InfoIcon />
                  </IconButton>
                  <IconButton onClick={() => editContact(contact.id)}>
                    <EditIcon />
                  </IconButton>
                  <IconButton>
                    <DeleteIcon />
                  </IconButton>
                </>
              }
            >
              <ListItemIcon>
                <PersonIcon />
              </ListItemIcon>
              <ListItemText
                primary={`${contact.first_name} ${contact.last_name}`}
                secondary={contact.phone_number}
              />
            </ListItem>
          </Box>
        ))}
      </List>
      <ContactInfoDialog
        contact={selectedContact}
        onClose={closeContactInfoModal}
        onEdit={
        	// we'll use this when we're ready to edit contacts
          selectedContact ? () => editContact(selectedContact.id) : () => {}
        }
        open={!!selectedContact}
      />
      <Fab
        color="primary"
        onClick={() => Inertia.visit('/contact/add')}
        sx={{ bottom: 20, position: 'absolute', right: 20 }}
      >
        <AddIcon />
      </Fab>
    </>
  );
};

export default Listing;

```

![Contact Modal](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777646/dev-tutorials/djir-contacts-p2/img-18_qxta3o.png)

Not bad at all for our contact information dialog, right? Let's proceed to the edit functionality and then we'll handle the delete.

* We're going to have a reusable notification component, so let's implement that right now `react-app/src/components/Notification.tsx`

```jsx
import React from 'react';
import type { FC } from 'react';
import type { AlertColor } from '@mui/material';
import { Alert, Snackbar } from '@mui/material';

interface NotificationProps {
  closeNotification: () => void;
  duration?: number;
  notificationOpen: boolean;
  notificationText: string;
  notificationType?: AlertColor;
}

export const Notification: FC<NotificationProps> = ({
  closeNotification,
  duration = 3000,
  notificationOpen,
  notificationType,
  notificationText,
}) => {
  return (
    <Snackbar
      anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
      autoHideDuration={duration}
      onClose={closeNotification}
      open={notificationOpen}
    >
      <Alert
        onClose={closeNotification}
        severity={notificationType}
        variant="filled"
      >
        {notificationText}
      </Alert>
    </Snackbar>
  );
};

```



* Let's go ahead and implement the edit contact page `react-app/src/pages/Contact/ContactForm.tsx`

```jsx
import React, { useCallback, useEffect, useState } from 'react';
import type { FC, FormEvent } from 'react';
import { Inertia } from '@inertiajs/inertia';
import { useForm } from '@inertiajs/inertia-react';
import {
  Box,
  Button,
  Container,
  FormControl,
  FormHelperText,
  Grid,
  Input,
  InputLabel,
  Typography,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import { ContactPhone as ContactIcon } from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
import { Notification } from '../../components/Notification';

type FormData = {
  first_name: string;
  middle_name: string;
  last_name: string;
  nickname: string;
  email_address: string;
  phone_number: string;
};

const SUBMIT_RESPONSE_MESSAGES: Record<AlertColor, string> = {
  success: 'Contact information was saved',
  error: 'There was a problem with saving contact information.',
  info: '',
  warning: '',
};

interface ContactFormProps {
  contact?: Contact;
}

const ContactForm: FC<ContactFormProps> = ({ contact }) => {
  const {
    clearErrors,
    data,
    errors,
    post: createContact,
    processing,
    setData,
    put: updateContact,
  } = useForm({
    first_name: '',
    middle_name: '',
    last_name: '',
    nickname: '',
    email_address: '',
    phone_number: '',
  });

  const [contactId, setContactId] = useState<number | undefined>();
  const [notificationType, setNotificationType] = useState<AlertColor>();
  const [notificationOpen, setNotificationOpen] = useState(false);
  const [notificationText, setNotificationText] = useState('');

  useEffect(() => {
    if (contact) {
      setContactId(contact.id);
      setData({
        ...contact,
        middle_name: contact.middle_name || '',
        nickname: contact.nickname || '',
      } as FormData);
    }
  }, [contact]);

  const closeNotification = useCallback(() => {
    if (notificationType === 'success') {
      Inertia.visit('/contact');
    }
    setNotificationOpen(false);
    setNotificationText('');
  }, [notificationType]);

  const handleNotification = useCallback((notificationType: AlertColor) => {
    setNotificationType(notificationType);
    setNotificationText(SUBMIT_RESPONSE_MESSAGES[notificationType]);
    setNotificationOpen(true);
  }, []);

  const submitContactData = useCallback(
    (e: FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      clearErrors();
      contact
        ? updateContact(`/contact/${contactId}/`, {
            onError: () => handleNotification('error'),
            onSuccess: () => {
              handleNotification('success');
              setTimeout(() => {
                Inertia.visit('/contact');
              }, 3000);
            },
          })
        : createContact('', {
            onError: () => handleNotification('error'),
            onSuccess: () => {
              handleNotification('success');
              setTimeout(() => {
                Inertia.visit('/contact');
              }, 3000);
            },
          });
    },
    [contactId, data]
  );

  return (
    <Container maxWidth="md" sx={{ pt: 2, width: '100%' }}>
      <Box
        sx={{
          alignItems: 'center',
          display: 'flex',
          flexDirection: 'row',
          mb: 2,
        }}
      >
        <ContactIcon fontSize="large" />
        <Typography sx={{ fontSize: 40, ml: 1 }} variant="h1">
          {contact ? 'Update' : 'Add New'} Contact
        </Typography>
      </Box>
      <form onSubmit={submitContactData}>
        <Grid container spacing={2}>
          <Grid item sm={4} xs={12}>
            <FormControl error={!!errors.first_name} fullWidth>
              <InputLabel htmlFor="first-name">First Name</InputLabel>
              <Input
                id="first-name"
                onChange={(e) => setData('first_name', e.target.value)}
                placeholder="First Name..."
                required
                value={data.first_name}
              />
              {errors.first_name ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.first_name}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={4} xs={12}>
            <FormControl error={!!errors.middle_name} fullWidth>
              <InputLabel htmlFor="middle-name">Middle Name</InputLabel>
              <Input
                id="middle-name"
                onChange={(e) => setData('middle_name', e.target.value)}
                placeholder="Middle Name..."
                type="text"
                value={data.middle_name}
              />
              {errors.middle_name ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.middle_name}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={4} xs={12}>
            <FormControl error={!!errors.last_name} fullWidth>
              <InputLabel htmlFor="last-name">Last Name</InputLabel>
              <Input
                id="last-name"
                onChange={(e) => setData('last_name', e.target.value)}
                placeholder="Last Name..."
                required
                value={data.last_name}
              />
              {errors.last_name ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.last_name}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item xs={12}>
            <FormControl error={!!errors.nickname} fullWidth>
              <InputLabel>Nickname</InputLabel>
              <Input
                error={!!errors.nickname}
                id="nickname"
                onChange={(e) => setData('nickname', e.target.value)}
                placeholder="Nickname..."
                value={data.nickname}
              />
              {errors.nickname ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.nickname}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={6} xs={12}>
            <FormControl error={!!errors.email_address} fullWidth>
              <InputLabel htmlFor="email-address">Email Address</InputLabel>
              <Input
                id="email-address"
                onChange={(e) => setData('email_address', e.target.value)}
                placeholder="user@example.com"
                required
                type="email"
                value={data.email_address}
              />
              {errors.email_address ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.email_address}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={6} xs={12}>
            <FormControl error={!!errors.phone_number} fullWidth>
              <InputLabel htmlFor="phone-number">Phone Number</InputLabel>
              <Input
                id="phone-number"
                onChange={(e) => setData('phone_number', e.target.value)}
                placeholder="555-0001"
                required
                value={data.phone_number}
              />
              {errors.phone_number ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.phone_number}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
        </Grid>
        <Box sx={{ mt: 2 }}>
          <Button
            disabled={processing}
            fullWidth
            type="submit"
            variant="contained"
          >
            {contact ? 'Update' : 'Save New'} Contact
          </Button>
        </Box>
      </form>
      <Notification
        closeNotification={closeNotification}
        notificationOpen={notificationOpen}
        notificationText={notificationText}
        notificationType={notificationType}
      />
    </Container>
  );
};

export default ContactForm;
```

* Next, let's set up our view so that we can navigate to the contact form by editing `contact/views.py`

```python
# Add the function to contact/views.py

@inertia('Contact/ContactForm')
def add_contact(request):
    if request.method == 'GET':
        return {}

    result = dataSource.upsert_contact(json.loads(request.body))
    return result
```

* Let's update our contact urls in `contact/urls.py`

```python
from django.urls import path

from contact import views

urlpatterns = [
    path('', views.contact_list, name='contact-list'),
		path('add/', views.add_contact, name='add-contact'),
]

```

* Let's edit our listing to add a `New Contact` button and hook it up

```jsx
// add these imports
import { Fab } from '@mui/material';
import { Inertia } from '@inertiajs/inertia';

// Add this near the bottom of the markup in the JSX 
<Fab
  color="primary"
  onClick={() => Inertia.visit('/contact/add')}
  sx={{ bottom: 20, position: 'absolute', right: 20 }}
>
  <AddIcon />
</Fab>
```

* Let's try try navigating to our newly created contact form by clicking the FAB we added

![contact form](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777649/dev-tutorials/djir-contacts-p2/img-19_qlim1x.png)

* Since we're using Django's Model Forms, we get backend validation that we can tap into. Let's try it out

:information_source: This would be a good time to also add in some Front-end validation with whatever method you would like.

![validation errors working](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777658/dev-tutorials/djir-contacts-p2/img-20_xc6x5t.png)

* Let's try to save a contact and see what happens

![Contact Added](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777662/dev-tutorials/djir-contacts-p2/img-22_sfimdl.png)

![Newly added contact details](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777656/dev-tutorials/djir-contacts-p2/img-23_ibvrua.png)

* Let's work on implementing the edit functionality by updating our `contact/views.py` file as follows

```python
# add this to end of the file
@inertia('Contact/ContactForm')
def contact(request, pk):
    if request.method == 'GET':
        data = dataSource.get_contact(pk)
        return {
            'success': True if data is not None else False,
            'contact': data
        }

    result = dataSource.upsert_contact(json.loads(request.body), pk)
    return result
```

* Update our urls `contact/url.py` file as follows

```python
from django.urls import path

from contact import views

urlpatterns = [
    path('', views.contact_list, name='contact-list'),
    path('add/', views.add_contact, name='add-contact'),
    path('<int:pk>/', views.contact, name='contact'), # <- we added this path
]
```

With this in place, we should be able to click the edit button from the listing or the contact info modal and have the contact form load with details. The save button should also work since we wired that up earlier. Go ahead and try it.

![Updating contacts work](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777649/dev-tutorials/djir-contacts-p2/img-24_wzutvl.png)

Let's move on to the delete functionality as no contact manager would be complete without delete functionality.

* Edit `contact/dataSource.py` by adding the function below to the end of the file

```python
def delete_contact(contact_id):
    result = {
        'errors': '',
        'success': False
    }
    try:
        Contact.objects.get(contact_id).delete()
        result['success'] = True
    except Exception as err:
        result['errors'] = err.__str__()

    return result
```

* Let's also update our views `contact/views.py` by adding the following code to the end of the file

```python
def delete_contact(request, pk):
    if request.method != 'DELETE':
        return redirect('contact_list')

    result = dataSource.delete_contact(pk)
    if not result['success']:
        return result
    
    return redirect('contact-list')
```

* Let's update our urls `contact/urls.py` so that it looks like what is below

```python
from django.urls import path

from contact import views

urlpatterns = [
    path('', views.contact_list, name='contact-list'),
    path('add/', views.add_contact, name='add-contact'),
    path('<int:pk>/', views.contact, name='contact'),
    path('<int:pk>/delete/', views.delete_contact), # <- we added this
]
```

* You guys might notice that I like using dialogs, so we're going to make a question dialog called `QuestionDialog` :smirk_cat: in our components folder. `react-app/src/components/QuestionDialog.tsx`

```jsx
import React from 'react';
import type { FC } from 'react';

import {
  Button,
  Dialog,
  DialogActions,
  DialogContentText,
  DialogTitle,
  DialogContent,
} from '@mui/material';
import { DeleteForeverOutlined as DeleteIcon } from '@mui/icons-material';
import type { Contact } from '../interfaces/contact.interface';

interface QuestionDialogProps {
  cancelAction: () => void;
  confirmAction: () => void;
  contact?: Contact;
  open: boolean;
}

export const QuestionDialog: FC<QuestionDialogProps> = ({
  cancelAction,
  confirmAction,
  contact,
  open,
}) => {
  return (
    <Dialog open={open} onClose={cancelAction}>
      <DialogTitle id="question-dialog-title">
        {`Delete Contact: ${contact?.first_name} ${contact?.last_name}`}?
      </DialogTitle>
      <DialogContent>
        <DialogContentText id="question-dialog-prompt-text">
          {contact
            ? `You are about to delete ${contact.first_name} ${
                contact.last_name
              } ${
                contact.nickname ? `[${contact.nickname}]` : ''
              }. Would you like to continue?`
            : ''}
        </DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button onClick={cancelAction}>No</Button>
        <Button
          onClick={confirmAction}
          startIcon={<DeleteIcon />}
          variant="contained"
        >
          Yes
        </Button>
      </DialogActions>
    </Dialog>
  );
};
```



:information_source: We could have made a dialog wrapper component since the structure and behavior of the dialogs are very similar (we can do that next time or, you could do it as a challenge)

* Let's update our contact listing as follows

```jsx
import React, { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
import { Inertia } from '@inertiajs/inertia';
import {
  Box,
  Divider,
  Fab,
  IconButton,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import {
  AddCircleOutlineRounded as AddIcon,
  DeleteForeverOutlined as DeleteIcon,
  EditRounded as EditIcon,
  InfoOutlined as InfoIcon,
  Person2Rounded as PersonIcon,
} from '@mui/icons-material';

import type { Contact } from '../../interfaces/contact.interface';
import { ContactInfoDialog } from '../../components/ContactInfoDialog';
import { QuestionDialog } from '../../components/QuestionDialog';
import { Notification } from '../../components/Notification';

type DialogType = 'info' | 'question' | 'none';

const CONTACT_LIST_MESSAGES: Record<AlertColor, string> = {
  error: 'An error occurred while trying to delete contact.',
  info: '',
  success: 'Contact was deleted successfully.',
  warning: '',
};

interface ListingProps {
  contacts: Contact[];
}

const Listing: FC<ListingProps> = ({ contacts }) => {
  const [selectedContactId, setSelectedContactId] = useState<
    number | undefined
  >();
  const [dialogType, setDialogType] = useState<DialogType>('none');
  const [notificationType, setNotificationType] = useState<AlertColor>();
  const [notificationOpen, setNotificationOpen] = useState(false);
  const [notificationText, setNotificationText] = useState('');

  const closeContactInfoModal = useCallback(() => {
    setSelectedContactId(undefined);
    setDialogType('none');
  }, []);

  const editContact = useCallback((contactId: number) => {
    Inertia.visit(`/contact/${contactId}`);
  }, []);

  const showContactInfo = useCallback((contactId: number) => {
    setDialogType('info');
    setSelectedContactId(contactId);
  }, []);

  const closeQuestionDialog = useCallback(() => {
    setDialogType('none');
    setSelectedContactId(undefined);
  }, []);

  const deleteContact = useCallback((contactId: number) => {
    Inertia.delete(`${contactId}/delete`, {
      onError: () => {
        setNotificationType('error');
        setNotificationText(CONTACT_LIST_MESSAGES['error']);
        setNotificationOpen(true);
      },
      onSuccess: () => {
        setNotificationType('success');
        setNotificationText(CONTACT_LIST_MESSAGES['success']);
        setNotificationOpen(true);
      },
    });
  }, []);

  const deleteContactPrompt = useCallback((contactId: number) => {
    setDialogType('question');
    setSelectedContactId(contactId);
  }, []);

  const resetNotification = useCallback(() => {
    setNotificationOpen(false);
    setNotificationText('');
    setNotificationType(undefined);
  }, []);

  const selectedContact: Contact | undefined = useMemo(() => {
    try {
      return contacts.find((contact) => contact.id === selectedContactId);
    } catch (e) {
      return undefined;
    }
  }, [contacts, selectedContactId]);

  return (
    <>
      <List sx={{ width: '100%' }}>
        {contacts.map((contact, index) => (
          <Box key={`item-${index}`}>
            {index ? <Divider /> : null}
            <ListItem
              key={`contact-${contact.id}`}
              alignItems="center"
              secondaryAction={
                <>
                  <IconButton onClick={() => showContactInfo(contact.id)}>
                    <InfoIcon />
                  </IconButton>
                  <IconButton onClick={() => editContact(contact.id)}>
                    <EditIcon />
                  </IconButton>
                  <IconButton onClick={() => deleteContactPrompt(contact.id)}>
                    <DeleteIcon />
                  </IconButton>
                </>
              }
            >
              <ListItemIcon>
                <PersonIcon />
              </ListItemIcon>
              <ListItemText
                primary={`${contact.first_name} ${contact.last_name}`}
                secondary={contact.phone_number}
              />
            </ListItem>
          </Box>
        ))}
      </List>
      <ContactInfoDialog
        contact={selectedContact}
        onClose={closeContactInfoModal}
        onEdit={
          selectedContact ? () => editContact(selectedContact.id) : () => {}
        }
        open={!!selectedContact && dialogType === 'info'}
      />
      <QuestionDialog
        cancelAction={closeQuestionDialog}
        confirmAction={
          selectedContact ? () => deleteContact(selectedContact.id) : () => {}
        }
        contact={selectedContact}
        open={!!selectedContact && dialogType === 'question'}
      />
      <Fab
        color="primary"
        onClick={() => Inertia.visit('/contact/add')}
        sx={{ bottom: 20, position: 'absolute', right: 20 }}
      >
        <AddIcon />
      </Fab>
      <Notification
        closeNotification={resetNotification}
        notificationOpen={notificationOpen}
        notificationText={notificationText}
        notificationType={notificationType}
      />
    </>
  );
};

export default Listing;

```



![Question Dialog](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777661/dev-tutorials/djir-contacts-p2/img-25_bl5uun.png)

At this point, we should be able to delete a record and have it actually disappear from our listing. Go ahead and test it. If it all worked correctly, then you should see something like the image below.

![Delete contact confirmation](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777662/dev-tutorials/djir-contacts-p2/img-26_j5o5hh.png)

Looking pretty good, right? At this point, everything should be working without issue, if that's the case, feel free celebrate.

* Now would be a good time to commit the code we wrote. You guys already know how to do that since you're pros.

## Upgrading to InertiaJS 1.x

We're now at the part where we can go ahead and upgrade our project to InertiaJs 1.x. We will have to do a fair bit of refactoring but I promise that it won't be difficult. You can review the InertiaJs migration docs [here](https://inertiajs.com/upgrade-guide).

* Make a new branch for the upgrade
* Go ahead and stop Vite (if already running)
* Remove the old InertiaJs dependencies

```shell
# we didn't use @inertiajs/server so it was never installed
npm remove @inertiajs/inertia @inertiajs/inertia-react @inertiajs/progress
```

* Install the new dependency

```shell
npm install -D @inertiajs/react
```



![Remove old deps, add new dep](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777668/dev-tutorials/djir-contacts-p2/img-27_zrhor6.png)

* In our `react-app/src/main.jsx` file, let's update our InertiaJs import

```tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
// Remove these lines
// - import { createInertiaApp } from '@inertiajs/inertia-react';
// - import { InertiaProgress } from '@inertiajs/progress';
// Add this line
import { createInertiaApp } from '@inertiajs/react';
import Layout from './components/Layout.tsx';

const pages = import.meta.glob('./pages/**/*.tsx');

document.addEventListener('DOMContentLoaded', () => {
  createInertiaApp({
    resolve: async (name) => {
      const page = (await pages[`./pages/${name}.tsx`]()).default;
      page.layout = page.layout || Layout;

      return page;
    },
    setup({ el, App, props }) {
      createRoot(el).render(<App {...props} />);
    },
  }).then();
});
```

* Let's update the code in `react-app/src/pages/Contact/Listing.tsx`. Here, we're updating our imports and replacing all usages of `Inertia.visit()` with `router.visit()` and `Inertia.delete()` with `router.delete()`

```tsx
import React, { useCallback, useMemo, useState } from 'react';
import type { FC } from 'react';
// import { Inertia } from '@inertiajs/inertia';
import { router } from '@inertiajs/react';
import {
  Box,
  Divider,
  Fab,
  IconButton,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import {
  AddCircleOutlineRounded as AddIcon,
  DeleteForeverOutlined as DeleteIcon,
  EditRounded as EditIcon,
  InfoOutlined as InfoIcon,
  Person2Rounded as PersonIcon,
} from '@mui/icons-material';

import type { Contact } from '../../interfaces/contact.interface';
import { ContactInfoDialog } from '../../components/ContactInfoDialog';
import { QuestionDialog } from '../../components/QuestionDialog';
import { Notification } from '../../components/Notification';

type DialogType = 'info' | 'question' | 'none';

const CONTACT_LIST_MESSAGES: Record<AlertColor, string> = {
  error: 'An error occurred while trying to delete contact.',
  info: '',
  success: 'Contact was deleted successfully.',
  warning: '',
};

interface ListingProps {
  contacts: Contact[];
}

const Listing: FC<ListingProps> = ({ contacts }) => {
  const [selectedContactId, setSelectedContactId] = useState<
    number | undefined
  >();
  const [dialogType, setDialogType] = useState<DialogType>('none');
  const [notificationType, setNotificationType] = useState<AlertColor>();
  const [notificationOpen, setNotificationOpen] = useState(false);
  const [notificationText, setNotificationText] = useState('');

  const closeContactInfoModal = useCallback(() => {
    setSelectedContactId(undefined);
    setDialogType('none');
  }, []);

  const editContact = useCallback((contactId: number) => {
    router.visit(`/contact/${contactId}`);
  }, []);

  const showContactInfo = useCallback((contactId: number) => {
    setDialogType('info');
    setSelectedContactId(contactId);
  }, []);

  const closeQuestionDialog = useCallback(() => {
    setDialogType('none');
    setSelectedContactId(undefined);
  }, []);

  const deleteContact = useCallback((contactId: number) => {
    router.delete(`${contactId}/delete`, {
      onError: () => {
        setNotificationType('error');
        setNotificationText(CONTACT_LIST_MESSAGES['error']);
        setNotificationOpen(true);
      },
      onSuccess: () => {
        setNotificationType('success');
        setNotificationText(CONTACT_LIST_MESSAGES['success']);
        setNotificationOpen(true);
      },
    });
  }, []);

  const deleteContactPrompt = useCallback((contactId: number) => {
    setDialogType('question');
    setSelectedContactId(contactId);
  }, []);

  const resetNotification = useCallback(() => {
    setNotificationOpen(false);
    setNotificationText('');
    setNotificationType(undefined);
  }, []);

  const selectedContact: Contact | undefined = useMemo(() => {
    try {
      return contacts.find((contact) => contact.id === selectedContactId);
    } catch (e) {
      return undefined;
    }
  }, [contacts, selectedContactId]);

  return (
    <>
      <List sx={{ width: '100%' }}>
        {contacts.map((contact, index) => (
          <Box key={`item-${index}`}>
            {index ? <Divider /> : null}
            <ListItem
              key={`contact-${contact.id}`}
              alignItems="center"
              secondaryAction={
                <>
                  <IconButton onClick={() => showContactInfo(contact.id)}>
                    <InfoIcon />
                  </IconButton>
                  <IconButton onClick={() => editContact(contact.id)}>
                    <EditIcon />
                  </IconButton>
                  <IconButton onClick={() => deleteContactPrompt(contact.id)}>
                    <DeleteIcon />
                  </IconButton>
                </>
              }
            >
              <ListItemIcon>
                <PersonIcon />
              </ListItemIcon>
              <ListItemText
                primary={`${contact.first_name} ${contact.last_name}`}
                secondary={contact.phone_number}
              />
            </ListItem>
          </Box>
        ))}
      </List>
      <ContactInfoDialog
        contact={selectedContact}
        onClose={closeContactInfoModal}
        onEdit={
          selectedContact ? () => editContact(selectedContact.id) : () => {}
        }
        open={!!selectedContact && dialogType === 'info'}
      />
      <QuestionDialog
        cancelAction={closeQuestionDialog}
        confirmAction={
          selectedContact ? () => deleteContact(selectedContact.id) : () => {}
        }
        contact={selectedContact}
        open={!!selectedContact && dialogType === 'question'}
      />
      <Fab
        color="primary"
        onClick={() => router.visit('/contact/add')}
        sx={{ bottom: 20, position: 'absolute', right: 20 }}
      >
        <AddIcon />
      </Fab>
      <Notification
        closeNotification={resetNotification}
        notificationOpen={notificationOpen}
        notificationText={notificationText}
        notificationType={notificationType}
      />
    </>
  );
};

export default Listing;

```



* Let's update the code in `react-app/src/pages/Contact/ContactForm.tsx`. Here, we're updating our imports and replacing all usages of `Inertia.visit()` with `router.visit()`. We don't have to change the usage of `useForm`.

```tsx
import React, { useCallback, useEffect, useState } from 'react';
import type { FC, FormEvent } from 'react';
// remove these lines
// - import { Inertia } from '@inertiajs/inertia';
// - import { useForm } from '@inertiajs/inertia-react';
// Add this line
import {router, useForm} from '@inertiajs/react';
import {
  Box,
  Button,
  Container,
  FormControl,
  FormHelperText,
  Grid,
  Input,
  InputLabel,
  Typography,
} from '@mui/material';
import type { AlertColor } from '@mui/material';
import { ContactPhone as ContactIcon } from '@mui/icons-material';
import type { Contact } from '../../interfaces/contact.interface';
import { Notification } from '../../components/Notification';

type FormData = {
  first_name: string;
  middle_name: string;
  last_name: string;
  nickname: string;
  email_address: string;
  phone_number: string;
};

const SUBMIT_RESPONSE_MESSAGES: Record<AlertColor, string> = {
  success: 'Contact information was saved',
  error: 'There was a problem with saving contact information.',
  info: '',
  warning: '',
};

interface ContactFormProps {
  contact?: Contact;
}

const ContactForm: FC<ContactFormProps> = ({ contact }) => {
  const {
    clearErrors,
    data,
    errors,
    post: createContact,
    processing,
    setData,
    put: updateContact,
  } = useForm({
    first_name: '',
    middle_name: '',
    last_name: '',
    nickname: '',
    email_address: '',
    phone_number: '',
  });

  const [contactId, setContactId] = useState<number | undefined>();
  const [notificationType, setNotificationType] = useState<AlertColor>();
  const [notificationOpen, setNotificationOpen] = useState(false);
  const [notificationText, setNotificationText] = useState('');

  useEffect(() => {
    if (contact) {
      setContactId(contact.id);
      setData({
        ...contact,
        middle_name: contact.middle_name || '',
        nickname: contact.nickname || '',
      } as FormData);
    }
  }, [contact]);

  const closeNotification = useCallback(() => {
    if (notificationType === 'success') {
      router.visit('/contact');
    }
    setNotificationOpen(false);
    setNotificationText('');
  }, [notificationType]);

  const handleNotification = useCallback((notificationType: AlertColor) => {
    setNotificationType(notificationType);
    setNotificationText(SUBMIT_RESPONSE_MESSAGES[notificationType]);
    setNotificationOpen(true);
  }, []);

  const submitContactData = useCallback(
    (e: FormEvent<HTMLFormElement>) => {
      e.preventDefault();
      clearErrors();
      contact
        ? updateContact(`/contact/${contactId}/`, {
            onError: () => handleNotification('error'),
            onSuccess: () => {
              handleNotification('success');
              setTimeout(() => {
                router.visit('/contact');
              }, 3000);
            },
          })
        : createContact('', {
            onError: () => handleNotification('error'),
            onSuccess: () => {
              handleNotification('success');
              setTimeout(() => {
                router.visit('/contact');
              }, 3000);
            },
          });
    },
    [contactId, data]
  );

  return (
    <Container maxWidth="md" sx={{ pt: 2, width: '100%' }}>
      <Box
        sx={{
          alignItems: 'center',
          display: 'flex',
          flexDirection: 'row',
          mb: 2,
        }}
      >
        <ContactIcon fontSize="large" />
        <Typography sx={{ fontSize: 40, ml: 1 }} variant="h1">
          {contact ? 'Update' : 'Add New'} Contact
        </Typography>
      </Box>
      <form onSubmit={submitContactData}>
        <Grid container spacing={2}>
          <Grid item sm={4} xs={12}>
            <FormControl error={!!errors.first_name} fullWidth>
              <InputLabel htmlFor="first-name">First Name</InputLabel>
              <Input
                id="first-name"
                onChange={(e) => setData('first_name', e.target.value)}
                placeholder="First Name..."
                required
                value={data.first_name}
              />
              {errors.first_name ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.first_name}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={4} xs={12}>
            <FormControl error={!!errors.middle_name} fullWidth>
              <InputLabel htmlFor="middle-name">Middle Name</InputLabel>
              <Input
                id="middle-name"
                onChange={(e) => setData('middle_name', e.target.value)}
                placeholder="Middle Name..."
                type="text"
                value={data.middle_name}
              />
              {errors.middle_name ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.middle_name}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={4} xs={12}>
            <FormControl error={!!errors.last_name} fullWidth>
              <InputLabel htmlFor="last-name">Last Name</InputLabel>
              <Input
                id="last-name"
                onChange={(e) => setData('last_name', e.target.value)}
                placeholder="Last Name..."
                required
                value={data.last_name}
              />
              {errors.last_name ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.last_name}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item xs={12}>
            <FormControl error={!!errors.nickname} fullWidth>
              <InputLabel>Nickname</InputLabel>
              <Input
                error={!!errors.nickname}
                id="nickname"
                onChange={(e) => setData('nickname', e.target.value)}
                placeholder="Nickname..."
                value={data.nickname}
              />
              {errors.nickname ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.nickname}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={6} xs={12}>
            <FormControl error={!!errors.email_address} fullWidth>
              <InputLabel htmlFor="email-address">Email Address</InputLabel>
              <Input
                id="email-address"
                onChange={(e) => setData('email_address', e.target.value)}
                placeholder="user@example.com"
                required
                type="email"
                value={data.email_address}
              />
              {errors.email_address ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.email_address}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
          <Grid item sm={6} xs={12}>
            <FormControl error={!!errors.phone_number} fullWidth>
              <InputLabel htmlFor="phone-number">Phone Number</InputLabel>
              <Input
                id="phone-number"
                onChange={(e) => setData('phone_number', e.target.value)}
                placeholder="555-0001"
                required
                value={data.phone_number}
              />
              {errors.phone_number ? (
                <FormHelperText sx={{ ml: 0 }}>
                  {errors.phone_number}
                </FormHelperText>
              ) : null}
            </FormControl>
          </Grid>
        </Grid>
        <Box sx={{ mt: 2 }}>
          <Button
            disabled={processing}
            fullWidth
            type="submit"
            variant="contained"
          >
            {contact ? 'Update' : 'Save New'} Contact
          </Button>
        </Box>
      </form>
      <Notification
        closeNotification={closeNotification}
        notificationOpen={notificationOpen}
        notificationText={notificationText}
        notificationType={notificationType}
      />
    </Container>
  );
};

export default ContactForm;

```

* Let's restart Vite and if everything worked as it should, we shouldn't notice any changes or errors.



![Inertia Upgraded and no errors](https://res.cloudinary.com/drcpiax9p/image/upload/v1674777667/dev-tutorials/djir-contacts-p2/img-28_nwgcif.png)

Hope you found this guide / tutorial useful. And of course, let me know if you have any questions or comments. There might be a Part 2.5 to this tutorial that would cover the Contact Notes section.

## References

The following resources were used in the creation of this part of the tutorial

* __InertiaJs migration guide__ https://inertiajs.com/upgrade-guide
* __Material UI for React__ https://mui.com/material-ui/getting-started/overview/
* __Django Model Form documentation__ https://docs.djangoproject.com/en/4.1/topics/forms/modelforms/