{"id":16968966,"url":"https://github.com/webmasterdevlin/tanstack-router-contact-app-starter","last_synced_at":"2025-07-30T20:04:51.731Z","repository":{"id":254978853,"uuid":"848121843","full_name":"webmasterdevlin/tanstack-router-contact-app-starter","owner":"webmasterdevlin","description":null,"archived":false,"fork":false,"pushed_at":"2024-09-19T18:15:34.000Z","size":94,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-30T09:53:10.183Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/webmasterdevlin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-27T07:06:04.000Z","updated_at":"2024-09-19T18:15:37.000Z","dependencies_parsed_at":null,"dependency_job_id":"c369aeb5-4a36-41f0-98a6-226c26e8d5ae","html_url":"https://github.com/webmasterdevlin/tanstack-router-contact-app-starter","commit_stats":null,"previous_names":["webmasterdevlin/tanstack-router-contact-app-starter"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/webmasterdevlin/tanstack-router-contact-app-starter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webmasterdevlin%2Ftanstack-router-contact-app-starter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webmasterdevlin%2Ftanstack-router-contact-app-starter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webmasterdevlin%2Ftanstack-router-contact-app-starter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webmasterdevlin%2Ftanstack-router-contact-app-starter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/webmasterdevlin","download_url":"https://codeload.github.com/webmasterdevlin/tanstack-router-contact-app-starter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webmasterdevlin%2Ftanstack-router-contact-app-starter/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267930611,"owners_count":24167475,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-30T02:00:09.044Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T00:23:35.185Z","updated_at":"2025-07-30T20:04:51.681Z","avatar_url":"https://github.com/webmasterdevlin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TanStack Router Tutorial\n- This is originated in [React Router Tutorial](https://reactrouter.com/en/main/start/tutorial)\n- data is saved in [indexedDB](https://dev.to/armstrong2035/9-differences-between-indexeddb-and-localstorage-30ai) in the browser.\n- you can use any package manager you like\n\n## Steps in building the project from the starter repo\n\n### Setting up the routes\n\n- [x] download the starter repo\n- [x] npm install\n- [x] create a folder named `routes` inside the `src` folder\n- [x] create a file named `__root.tsx` inside the `routes` folder\n```tsx\nimport { createRootRoute } from \"@tanstack/react-router\";\n\nexport const Route = createRootRoute({\n    component: RootComponent\n});\n\nfunction RootComponent() {\n    return (\n        \u003cdiv\u003e\n            \u003ch1\u003eHello from React and TanStack Router\u003c/h1\u003e\n        \u003c/div\u003e\n    );\n}\n```\n- [x] import and add `TanStackRouterVite()` in vite.config.ts's `plugins` array\n- [x] install TanStack Router CLI\n```bash\nnpm i -g @tanstack/router-cli\n``` \n- [x] run the TanStack Router CLI to generate `routeTree.gen.ts` file\n```bash\ntsr generate\n```\nThe 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.\n- [x] run the tsr watcher to watch for changes in the `routeTree.gen.ts` file\n```bash\ntsr watch\n```\n- [x] run the project\n```bash\nnpm run dev\n```\n- [x] finish the `App.tsx` file to integrate the typesafe routes.\n- [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.\n- [x] update the `__root.tsx` file to add the `Outlet` component.\n```tsx\n\u003c\u003e\n  \u003cdiv id=\"detail\"\u003e\n    \u003cOutlet /\u003e\n  \u003c/div\u003e\n\u003c/\u003e\n```\n- [x] see the changes in the browser\n\n### Adding the TanStack Router Devtools\n\n- [x] update the `__root.tsx` file to add the `TanStackRouterDevtools` component.\n```tsx\nconst TanStackRouterDevtools =\n  process.env.NODE_ENV === 'production'\n    ? () =\u003e null // Render nothing in production\n    : lazy(() =\u003e\n        // Lazy load in development\n        import('@tanstack/router-devtools').then((res) =\u003e ({\n          default: res.TanStackRouterDevtools,\n          // For Embedded Mode\n          // default: res.TanStackRouterDevtoolsPanel\n        }))\n      );\n```\n- [x] then above the RootComponent function, add the following code:\n```tsx\n\u003c\u003e\n  \u003cdiv id=\"detail\"\u003e\n    \u003cOutlet /\u003e\n  \u003c/div\u003e\n  \u003cSuspense\u003e\n    \u003cTanStackRouterDevtools position=\"bottom-right\" /\u003e\n  \u003c/Suspense\u003e\n\u003c/\u003e\n```\n- [x] see the changes in the browser\n\n### Adding New Contact functionality\n\n- [x] update the `__root.tsx` file to add sidebar and the list of contacts.\n```tsx\n\u003c\u003e\n  \u003cdiv id=\"sidebar\"\u003e\n    \u003cSidebarFooter /\u003e\n  \u003c/div\u003e\n  \u003cdiv id=\"detail\"\u003e\n    \u003cOutlet /\u003e\n  \u003c/div\u003e\n  \u003cSuspense\u003e\n    \u003cTanStackRouterDevtools position=\"bottom-right\" /\u003e\n  \u003c/Suspense\u003e\n\u003c/\u003e\n```\n- [x] import and place the `\u003cSidebarSearchContact /\u003e` below the SideBarFooter component.\n```tsx\n\u003cSidebarSearchContact /\u003e\n```\n- [x] open the chrome devtools and go to the `Application` tab\n- [x] go to the indexedDB of the application\n- [x] go the web app and click the `New` button that will trigger the `createContact` function.\n- [x] confirm that the new contact is added to the indexedDB\n\n### Showing the list of contacts\n\n- [x] update the `RootRouteOptions` of the `__root.tsx` file.\n```tsx\nexport const Route = createRootRoute({\n  component: RootComponent,\n  validateSearch: z.object({\n    q: z.string().optional(),\n  }),\n  // eslint-disable-next-line sort-keys-fix/sort-keys-fix\n  loaderDeps: ({ search: { q } }) =\u003e {\n    return { q };\n  },\n  // eslint-disable-next-line sort-keys-fix/sort-keys-fix\n  loader: async ({ deps: { q } }) =\u003e {\n    const contacts = (await getContacts(q || '')) as Contact[];\n\n    return { contacts, q };\n  },\n});\n```\n- [x] add the `\u003cSidebarContactList /\u003e` below the SidebarSearchContact component.\n```tsx\n\u003cSidebarContactList /\u003e\n```\n- [x] see the changes in the browser. It says **no contacts**.\n- [x] replace the placeholder of contacts with the `Route` instance from the `__root.tsx` file.\n```tsx\nconst { contacts } = Route.useLoaderData();\n```\n- [x] see the changes in the browser. It should now show the list of contacts with one object (no name).\n\n### Creating a navigation functionality\n- [x] create a new page named `contacts.$contactId.index.tsx` inside the `routes` folder then save the file to reload your IDE.\n- [x] check the `routeTree.gen.ts` file. It should have the new route.\n- [x] change the `ahref tag` to use the `Link` component from TanStack Router.\n```tsx\n\u003cLink to={`/contacts/`}\u003e\u003c/Link\u003e\n```\n- [x] hover over the to prop and see the type of the prop. It should be a set of union type of strings.\n- [x] update the `Link` with this.\n```tsx\n\u003cLink to={`/contacts/${contact.id}`}\u003e\u003c/Link\u003e\n```\n- [x] go to the browser and click on the contact. It should navigate to the empty contact detail page.\n\n### Adding the Contact Detail functionality\n\n- [x] update the `contacts.$contactId.index.tsx` file to show the contact details.\n```tsx\nimport { createFileRoute, notFound } from '@tanstack/react-router';\nimport { z } from 'zod';\n\nimport { getContact } from '../services/contacts';\nimport ContactDetail from '../components/ContactDetail';\nimport NotFoundPage from '../components/NotFoundPage';\nimport ErrorPage from '../components/ErrorPage';\n\nexport const Route = createFileRoute('/contacts/$contactId/')({\n  component: () =\u003e \u003cdiv\u003eHello /contacts/$contactId/!\u003c/div\u003e,\n  notFoundComponent: () =\u003e \u003cNotFoundPage message={\"Can't find contact\"} /\u003e,\n  errorComponent: () =\u003e \u003cErrorPage message={'Network error'} /\u003e,\n  params: {\n    parse: (params) =\u003e {\n      return {\n        contactId: z.string().parse(params.contactId),\n      };\n    },\n    stringify: ({ contactId }) =\u003e {\n      return { contactId: `${contactId}` };\n    },\n  },\n  // eslint-disable-next-line sort-keys-fix/sort-keys-fix\n  loader: async ({ params: { contactId } }) =\u003e {\n    const contact = await getContact(contactId as string);\n    if (!contact) {\n      throw notFound({ _global: false });\n    }\n\n    return contact;\n  },\n});\n```\n- [x] In the same file, separate the component and import the `ContactDetail` component like this:\n```tsx\nfunction ContactIdIndexComponent() {\n  return (\u003cContactDetail /\u003e);\n}\n```\n- [x] Still in the same file, update the component prop of the Route instance to use the `ContactIdIndexComponent` function.\n```tsx\n  component: ContactIdIndexComponent,\n```\n- [x] go to the `ContactDetail.tsx` and replace the contact placeholder with the `Route` instance from the `contacts.$contactId.index.tsx` file.\n```tsx\nconst contact = Route.useLoaderData();\n```\n\n### Adding delete functionality\n\n- [x] replace the params placeholder with this:\n```tsx\nconst params = Route.useParams();\n```\n- [x] add the the **Route's** `useNavigate` hook to the component.\n```tsx\nconst navigate = Route.useNavigate();\n```\n- [x] update the **handleDelete** function to use the **params** and  **navigate** function.\n```tsx\n  const handleDeleteEvent = async (e: FormEvent\u003cHTMLFormElement\u003e) =\u003e {\n    e.preventDefault();\n    if (globalThis.confirm('Please confirm you want to delete this record.')) {\n      await deleteContact(params.contactId);\n      await navigate({\n        to: '/',\n      });\n    }\n  };\n```\n- [x] go to the browser and click on the delete button. It should delete the contact and navigate to the home page.\n- [x] add more contacts and delete them to see the changes in the browser.\n\n### Adding the Edit functionality\n- [x] create a new page named `contacts.$contactId.edit.tsx` inside the `routes` folder then save the file to reload your IDE.\n- [x] update the `contacts.$contactId.edit.tsx` file to fetch the contact details.\n```tsx\nimport { createFileRoute, notFound } from '@tanstack/react-router';\nimport { getContact } from '../services/contacts';\nimport EditContactForm from '../components/EditContactForm';\n\nexport const Route = createFileRoute('/contacts/$contactId/edit')({\n  component: () =\u003e \u003cdiv\u003eHello /contacts/$contactId/edit!\u003c/div\u003e,\n  loader: async ({ params: { contactId } }) =\u003e {\n    const contact = await getContact(contactId);\n    if (!contact) {\n      throw notFound({ _global: false });\n    }\n    return contact;\n  },\n});\n```\n- [x] In the same file, separate the component and import the `EditContactForm` component like this:\n```tsx\nfunction EditContactComponent() {\n  return (\u003cEditContactForm /\u003e);\n}\n```\n- [x] Still in the same file, update the component prop of the Route instance to use the `EditContactComponent` function.\n```tsx\n  component: EditContactComponent,\n```\n- [x] we will need again the 3 Route hooks. Place them in the `EditContactForm.tsx` file.\n```tsx\n  const contact = Route.useLoaderData();\n  const params = Route.useParams();\n  const navigate = Route.useNavigate();\n```\n- [x] update the `handleOnSubmit` function to the **params** and the **navigate**.\n```tsx\n  const handleOnSubmit = async (event: FormEvent) =\u003e {\n    event.preventDefault();\n    const form = event.currentTarget as HTMLFormElement;\n    const formData = new FormData(form);\n    const updates = Object.fromEntries(formData.entries());\n    await updateContact(params.contactId as string, updates);\n    await navigate({\n      to: `/contacts/${params.contactId}`,\n    });\n  };\n```\n- [x] Go the browser and click a **no name** contact. Add a `/edit` to the url and see the edit form.\n- [x] Try to update the details of the contact and see the changes in the browser.\n\n### Adding navigation going to the edit form of the contact\n\n- [x] go back to the `ContactDetail.tsx` file and update the `handleEditEvent` function with this:\n```tsx\n  const handleEditEvent = async (event: FormEvent) =\u003e {\n    event.preventDefault();\n    await navigate({\n      to: `/contacts/${params.contactId}/edit`,\n    });\n  };\n```\n- [x] go back also to the `SidebarSearchContact.tsx` file. Import the `Route` from the `__root.tsx` and add this hook to the component.\n```tsx\n  const navigate = Route.useNavigate();\n```\n- [x] update the `handleOnSubmit` with this logic.\n```tsx\nconst contact = await createContact();\n    await navigate({\n      to: `/contacts/${contact.id}/edit`,\n    });\n```\n- [x] add this inside the `onClick` event of the cancel button.\n```tsx\n navigate({\n              to: `/contacts/${contact.id}`,\n            })\n```\n- [x] go to the browser and click the **New** button. It should navigate to the edit form of the new contact.\n\n### Adding search functionality\n\n- [x] go to the `__root.tsx` file and the hooks:\n```tsx\n  const { q } = Route.useLoaderData();\n  const [query, setQuery] = useState(q ?? '');\n  const router = useRouter();\n\n  useEffect(() =\u003e {\n    if (q) setQuery(q);\n  }, [q]);\n  ```\n- [x] update the `div` tag of the `\u003cOutlet /\u003e` with this:\n```tsx\n\u003cdiv id=\"detail\" className={router.state.isLoading ? 'loading' : ''}\u003e\n        \u003cOutlet /\u003e\n\u003c/div\u003e\n```\n- [x] add `query` and `setQuery` props to the **SidebarSearchContact** component.\n```tsx\n\u003cSidebarSearchContact query={query} setQuery={setQuery} /\u003e\n```\n- [x] go to the `SidebarSearchContact.tsx` file and add the props to the component.\n```tsx\nfunction SidebarSearchContact({ query, setQuery }: Props)\n```\n- [x] update the `handleOnChangeEvent` function with this:\n```tsx\n  const handleOnChangeEvent = async (e: FormEvent\u003cHTMLInputElement\u003e) =\u003e {\n    setQuery(e.currentTarget.value);\n    await navigate({ search: { q: e.currentTarget.value } });\n  };\n  ```\n- [x] import the `useRouter` hook to the component.\n```tsx\n  const router = useRouter();\n```\n- [x] add two more properties to the `input` tag.\n```tsx\nvalue={query}\nclassName={router.state.isLoading ? 'loading' : ''}\n```\n- [x] replace the **hidden** property of the `div` tag with an id `id=\"search-spinner\"` with this:\n```tsx\nhidden={!router.state.isLoading}\n```\n- [x] go to the browser and type a name in the search bar. It should show the loading spinner and the list of contacts.\n\n### Adding the star functionality\n- [x] go to the `Favorite.tsx` file and add the `useRouter` hook.\n```tsx\nconst router = useRouter();\n```\n- then update the `onSubmit` function with this after or below the `updateContact` function. The invalidation will trigger the loader to fetch the data again.:\n```tsx\nawait router.invalidate();\n```\n\n### End of the TanStack Router Tutorial\n\n- Thank you for following the steps. You can now explore more of the TanStack Router features and functionalities.\n- You can also check the [TanStack Router documentation](https://tanstack.com/router/latest) for more information.\n- Happy coding! 🚀\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebmasterdevlin%2Ftanstack-router-contact-app-starter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebmasterdevlin%2Ftanstack-router-contact-app-starter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebmasterdevlin%2Ftanstack-router-contact-app-starter/lists"}