https://github.com/webmasterdevlin/tanstack-router-contact-app
https://github.com/webmasterdevlin/tanstack-router-contact-app
Last synced: about 1 year ago
JSON representation
- Host: GitHub
- URL: https://github.com/webmasterdevlin/tanstack-router-contact-app
- Owner: webmasterdevlin
- License: mit
- Created: 2024-08-26T12:40:27.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2025-03-06T08:14:28.000Z (over 1 year ago)
- Last Synced: 2025-03-06T09:33:25.704Z (over 1 year ago)
- Language: TypeScript
- Size: 956 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# TanStack Router Tutorial
- This is originated in [React Router Tutorial](https://reactrouter.com/en/main/start/tutorial)
- data is saved in [indexedDB](https://dev.to/armstrong2035/9-differences-between-indexeddb-and-localstorage-30ai) in the browser.
- you can use any package manager you like
## Steps in building the project from the starter repo
### Setting up the routes
- [x] download the starter repo
- [x] npm install
- [x] create a folder named `routes` inside the `src` folder
- [x] create a file named `__root.tsx` inside the `routes` folder
```tsx
import { createRootRoute } from "@tanstack/react-router";
export const Route = createRootRoute({
component: RootComponent
});
function RootComponent() {
return (
Hello from React and TanStack Router
);
}
```
- [x] import and add `TanStackRouterVite()` in vite.config.ts's `plugins` array
- [x] install TanStack Router CLI
```bash
npm i -g @tanstack/router-cli
```
- [x] run the TanStack Router CLI to generate `routeTree.gen.ts` file
```bash
tsr generate
```
The tsr generate command will generate a `routeTree.gen.ts` file in the `src` folder. Normally in React Router, we would have to manually create the routes and nested routes. But with TanStack Router, we can generate the routes and nested routes using the CLI.
- [x] run the tsr watcher to watch for changes in the `routeTree.gen.ts` file
```bash
tsr watch
```
- [x] run the project
```bash
npm run dev
```
- [x] finish the `App.tsx` file to integrate the typesafe routes.
- [x] add an `index.tsx` file inside the `routes` folder. This file will be the entry point for the routes and boilerplate will be generated by the CLI for you.
- [x] update the `__root.tsx` file to add the `Outlet` component.
```tsx
<>
>
```
- [x] see the changes in the browser
### Adding the TanStack Router Devtools
- [x] update the `__root.tsx` file to add the `TanStackRouterDevtools` component.
```tsx
const TanStackRouterDevtools =
process.env.NODE_ENV === 'production'
? () => null // Render nothing in production
: lazy(() =>
// Lazy load in development
import('@tanstack/router-devtools').then((res) => ({
default: res.TanStackRouterDevtools,
// For Embedded Mode
// default: res.TanStackRouterDevtoolsPanel
}))
);
```
- [x] then above the RootComponent function, add the following code:
```tsx
<>
>
```
- [x] see the changes in the browser
### Adding New Contact functionality
- [x] update the `__root.tsx` file to add sidebar and the list of contacts.
```tsx
<>
>
```
- [x] import and place the `` below the SideBarFooter component.
```tsx
```
- [x] open the chrome devtools and go to the `Application` tab
- [x] go to the indexedDB of the application
- [x] go the web app and click the `New` button that will trigger the `createContact` function.
- [x] confirm that the new contact is added to the indexedDB
### Showing the list of contacts
- [x] update the `RootRouteOptions` of the `__root.tsx` file.
```tsx
export const Route = createRootRoute({
component: RootComponent,
validateSearch: z.object({
q: z.string().optional(),
}),
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
loaderDeps: ({ search: { q } }) => {
return { q };
},
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
loader: async ({ deps: { q } }) => {
const contacts = (await getContacts(q || '')) as Contact[];
return { contacts, q };
},
});
```
- [x] add the `` below the SidebarSearchContact component.
```tsx
```
- [x] see the changes in the browser. It says **no contacts**.
- [x] replace the placeholder of contacts with the `Route` instance from the `__root.tsx` file.
```tsx
const { contacts } = Route.useLoaderData();
```
- [x] see the changes in the browser. It should now show the list of contacts with one object (no name).
### Creating a navigation functionality
- [x] create a new page named `contacts.$contactId.index.tsx` inside the `routes` folder then save the file to reload your IDE.
- [x] check the `routeTree.gen.ts` file. It should have the new route.
- [x] change the `ahref tag` to use the `Link` component from TanStack Router.
```tsx
```
- [x] hover over the to prop and see the type of the prop. It should be a set of union type of strings.
- [x] update the `Link` with this.
```tsx
```
- [x] go to the browser and click on the contact. It should navigate to the empty contact detail page.
### Adding the Contact Detail functionality
- [x] update the `contacts.$contactId.index.tsx` file to show the contact details.
```tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { z } from 'zod';
import { getContact } from '../services/contacts';
import ContactDetail from '../components/ContactDetail';
import NotFoundPage from '../components/NotFoundPage';
import ErrorPage from '../components/ErrorPage';
export const Route = createFileRoute('/contacts/$contactId/')({
component: () =>
Hello /contacts/$contactId/!,
notFoundComponent: () => ,
errorComponent: () => ,
params: {
parse: (params) => {
return {
contactId: z.string().parse(params.contactId),
};
},
stringify: ({ contactId }) => {
return { contactId: `${contactId}` };
},
},
// eslint-disable-next-line sort-keys-fix/sort-keys-fix
loader: async ({ params: { contactId } }) => {
const contact = await getContact(contactId as string);
if (!contact) {
throw notFound({ _global: false });
}
return contact;
},
});
```
- [x] In the same file, separate the component and import the `ContactDetail` component like this:
```tsx
function ContactIdIndexComponent() {
return ();
}
```
- [x] Still in the same file, update the component prop of the Route instance to use the `ContactIdIndexComponent` function.
```tsx
component: ContactIdIndexComponent,
```
- [x] go to the `ContactDetail.tsx` and replace the contact placeholder with the `Route` instance from the `contacts.$contactId.index.tsx` file.
```tsx
const contact = Route.useLoaderData();
```
### Adding delete functionality
- [x] replace the params placeholder with this:
```tsx
const params = Route.useParams();
```
- [x] add the the **Route's** `useNavigate` hook to the component.
```tsx
const navigate = Route.useNavigate();
```
- [x] update the **handleDelete** function to use the **params** and **navigate** function.
```tsx
const handleDeleteEvent = async (e: FormEvent) => {
e.preventDefault();
if (globalThis.confirm('Please confirm you want to delete this record.')) {
await deleteContact(params.contactId);
await navigate({
to: '/',
});
}
};
```
- [x] go to the browser and click on the delete button. It should delete the contact and navigate to the home page.
- [x] add more contacts and delete them to see the changes in the browser.
### Adding the Edit functionality
- [x] create a new page named `contacts.$contactId.edit.tsx` inside the `routes` folder then save the file to reload your IDE.
- [x] update the `contacts.$contactId.edit.tsx` file to fetch the contact details.
```tsx
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getContact } from '../services/contacts';
import EditContactForm from '../components/EditContactForm';
export const Route = createFileRoute('/contacts/$contactId/edit')({
component: () =>
Hello /contacts/$contactId/edit!,
loader: async ({ params: { contactId } }) => {
const contact = await getContact(contactId);
if (!contact) {
throw notFound({ _global: false });
}
return contact;
},
});
```
- [x] In the same file, separate the component and import the `EditContactForm` component like this:
```tsx
function EditContactComponent() {
return ();
}
```
- [x] Still in the same file, update the component prop of the Route instance to use the `EditContactComponent` function.
```tsx
component: EditContactComponent,
```
- [x] we will need again the 3 Route hooks. Place them in the `EditContactForm.tsx` file.
```tsx
const contact = Route.useLoaderData();
const params = Route.useParams();
const navigate = Route.useNavigate();
```
- [x] update the `handleOnSubmit` function to the **params** and the **navigate**.
```tsx
const handleOnSubmit = async (event: FormEvent) => {
event.preventDefault();
const form = event.currentTarget as HTMLFormElement;
const formData = new FormData(form);
const updates = Object.fromEntries(formData.entries());
await updateContact(params.contactId as string, updates);
await navigate({
to: `/contacts/${params.contactId}`,
});
};
```
- [x] Go the browser and click a **no name** contact. Add a `/edit` to the url and see the edit form.
- [x] Try to update the details of the contact and see the changes in the browser.
### Adding navigation going to the edit form of the contact
- [x] go back to the `ContactDetail.tsx` file and update the `handleEditEvent` function with this:
```tsx
const handleEditEvent = async (event: FormEvent) => {
event.preventDefault();
await navigate({
to: `/contacts/${contactId}/edit`, // add contact.id to the URL
params: { contactId:${params.contactId} },
});
};
```
- [x] go back also to the `SidebarSearchContact.tsx` file. Import the `Route` from the `__root.tsx` and add this hook to the component.
```tsx
const navigate = Route.useNavigate();
```
- [x] update the `handleOnSubmit` with this logic.
```tsx
const contact = await createContact();
await navigate({
to: `/contacts/${contact.id}/edit`,
});
```
- [x] add this inside the `onClick` event of the cancel button.
```tsx
navigate({
to: `/contacts/${contact.id}`,
})
```
- [x] go to the browser and click the **New** button. It should navigate to the edit form of the new contact.
### Adding search functionality
- [x] go to the `__root.tsx` file and the hooks:
```tsx
const { q } = Route.useLoaderData();
const [query, setQuery] = useState(q ?? '');
const router = useRouter();
useEffect(() => {
if (q) setQuery(q);
}, [q]);
```
- [x] update the `div` tag of the `` with this:
```tsx
```
- [x] add `query` and `setQuery` props to the **SidebarSearchContact** component.
```tsx
```
- [x] go to the `SidebarSearchContact.tsx` file and add the props to the component.
```tsx
function SidebarSearchContact({ query, setQuery }: Props)
```
- [x] update the `handleOnChangeEvent` function with this:
```tsx
const handleOnChangeEvent = async (e: FormEvent) => {
setQuery(e.currentTarget.value);
await navigate({ search: { q: e.currentTarget.value } });
};
```
- [x] import the `useRouter` hook to the component.
```tsx
const router = useRouter();
```
- [x] add two more properties to the `input` tag.
```tsx
value={query}
className={router.state.isLoading ? 'loading' : ''}
```
- [x] replace the **hidden** property of the `div` tag with an id `id="search-spinner"` with this:
```tsx
hidden={!router.state.isLoading}
```
- [x] go to the browser and type a name in the search bar. It should show the loading spinner and the list of contacts.
### Adding the star functionality
- [x] go to the `Favorite.tsx` file and add the `useRouter` hook.
```tsx
const router = useRouter();
```
- then update the `onSubmit` function with this after or below the `updateContact` function. The invalidation will trigger the loader to fetch the data again.:
```tsx
await router.invalidate();
```
### End of the TanStack Router Tutorial
- Thank you for following the steps. You can now explore more of the TanStack Router features and functionalities.
- You can also check the [TanStack Router documentation](https://tanstack.com/router/latest) for more information.
- Happy coding! 🚀