Web Application Implementation Guide
This guide introduces recommended application implementation patterns using React Router v7 (Framework mode).
Implementing TODO List Display and State Update Functionality
We will implement the functionality to display a TODO List and update its state. While the implementation includes save button functionality after updating todos, this catalog doesn't perform actual database operations but simulates the process by showing loaders up to the point where such operations would occur.
-
Welcome page
/
-
Todo list page
/todolist
The catalog AMI already contains the implemented state, and this document allows you to trace through the flow of how it was implemented.
Prerequisites
Implementation methods for basic components like root.tsx and layout are partially omitted. However, the content of this catalog is almost identical to what's generated when creating a new React Router v7 project.
Overall Process Flow
As a recommended configuration, we will implement in the following order, assuming after the initial setup of the entire project.
Target File | Implementation Summary |
---|---|
src/types/todo.d.ts | Define Todo types |
src/states/IsLoading.ts | Define data processing state |
src/hooks/useTodoSaver.tsx | Define custom hook for saving Todos |
src/components/TodoListItem.tsx | Implement TodoList items as reusable components |
src/pages/todolist/index.pages.tsx | Define TodoList page UI |
src/routes/todolist.tsx | TodoList page entry point and server-side processing (loader, action, screen entry point) |
src/layouts/index.pages.tsx | Implement app-wide layout |
src/routes.ts | Define TodoList page routing |
Additionally, please configure directories and files as needed.
src/types/todo.d.ts
Define Todo Types
Define types as follows. These will be used for Props definitions in each component and type declarations for source data.
export type Todo = {
id: string
title: string
status: 'pending' | 'inProgress' | 'completed'
isStarred: boolean
// ...
}
Also, defining the following in src/types/index.ts
simplifies import statements, so it's recommended to create this in all directories under src/
.
export * from './Todo.d'
src/states/IsLoading.ts
Define Data Processing State
Declare IsLoading state as follows. This catalog uses the Jotai library.
import { atom } from 'jotai'
export const IsLoadingAtom = atom<boolean>(false)
src/hooks/useTodoSaver.tsx
Define Custom Hook for Saving Todos
Implement the process for saving Todo changes. While this catalog doesn't perform actual update processing, it implements sending updated Todos to PUT /todolist
and logging the output.
import { useAtom } from 'jotai'
import * as Atoms from '~/states'
export const useTodoSaver = () => {
const [isLoading, setIsLoading] = useAtom(Atoms.IsLoadingAtom)
const saveTodo = async (todo: Todo) => {
setIsLoading(true)
const saveResult = fetch('/todolist', {
method: 'PUT',
headers: { 'content-type': 'application/json', body: JSON.stringify(todo) },
})
.then((res) => res.json())
.finally(() => setIsLoading(false))
console.log('## saveResult - ', saveResult)
}
return { isLoading, saveTodo }
}
src/components/TodoListItem.tsx
Implement TodoList Items as Reusable Components
Implement Todo list items. Such list item components are positioned and created with reusability for other screens in mind.
import type { Todo } from '~/types'
type Props = {
todo: Todo
}
export const TodoListItem: React.FC<Props> = ({ todo }) => {
// Some dynamic ui handler methods
const onClickSave = async () => {
await saveTodo(todo).finally(() => {
toast('Info', {
description: `Sucessfully saved.`,
action: {
label: 'Close',
onClick: () => {
/* Closing toast. Nothing to do here. */
},
},
})
})
}
return (
<div className="bg-white rounded-lg border border-gray-200 p-6 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-3">
<h3 className="text-lg font-medium text-gray-900 truncate">{todo.title}</h3>
{ /* Omitted for brevity. All sample code is included in the catalog AMI. */ }
</div>
</div>
</div>
</div>
)
}
src/pages/todolist/index.pages.tsx
Define TodoList Page UI
Implement the UI for the TodoList page that displays the Todo list.
import { TodoListItem } from '~/components'
import type { Todo } from '~/types'
type Props = {
items: Todo[]
}
export const TodoListPage: React.FC<Props> = (props) => {
const { items } = props
return (
<>
<main className="max-w-6xl mx-auto px-4 py-8">
{ /* Omitted for brevity. All sample code is included in the catalog AMI. */ }
<div className="space-y-4">
{items.map((todo) => (
<TodoListItem key={todo.id} todo={todo} />
))}
</div>
</main>
</>
)
}
TodoList Entry Point and Server-side Processing Implementation
Under src/routes/
, you can configure server-side processing (loader, action) and various settings like meta tags when loading the corresponding screen. It's possible to fetch data from the server side and pass it to the Page component from the previous section via Props or other means. You can also implement loaders on separate routes and take a SPA-like approach asynchronously with GET /api/todolist
.
import type { MetaFunction } from 'react-router'
import type { Route } from './+types/todolist'
import type { Todo } from '~/types'
import { TODO_ITEMS } from '~/fixtures'
import { TodoListPage } from '~/pages'
export const meta: MetaFunction = () => {
// biome-ignore format: for better readability
return [
{ title: 'New React Router App - Todo List Page' },
{ name: 'description', content: 'Welcome to React Router!' }
]
}
export async function loader(params: Route.LoaderArgs) {
// For GET requests, this loader is called on the server side.
return { TODO_ITEMS }
}
export const action = async (_params: Route.ActionArgs) => {
// For POST, PUT, DELETE requests, this action is called on the server side.
// sleep here for 1 seconds for demo purpose
await sleep()
// Return normal API response
return new Response(JSON.stringify({ message: 'action is called' }), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
})
const TodoListRoute = ({ loaderData }: { loaderData: { TODO_ITEMS: Todo[] } }) => {
// Pass data retrieved from the loader to client-side components (under pages).
const { TODO_ITEMS } = loaderData
return <TodoListPage items={TODO_ITEMS} />
}
export default TodoListRoute
For details, please refer to the React Router v7 official documentation.
Implement App-wide Layout (Reflecting Loader)
Create src/layouts/default.ts
and implement the layout applied to the entire application. You can appropriately specify which layout to define in the next section's routes.ts
.
import { useAtom } from 'jotai'
import { Outlet, useLocation } from 'react-router'
import { Footer, Header, Loader } from '~/components'
import { Toaster } from '~/lib/shadcn-ui'
import * as Atoms from '~/states'
export const DefaultLayout = () => {
const [isLoading] = useAtom(Atoms.IsLoadingAtom)
const { pathname } = useLocation()
return (
<div className="min-h-screen">
<Header />
{ /* In this catalog, Loader is applied to layout */ }
{ /* Please place it in an appropriate component as needed. */ }
{isLoading && (
<div className="flex h-screen w-screen items-center justify-center overflow-scroll">
<Loader isLoading={isLoading} color="blue" />
</div>
)}
{!isLoading && (
<main key={pathname} className="min-h-screen">
<div className="bg-no-repeat bg-left-top">
{ /* Pages are reflected in the Outlet */ }
<Outlet />
</div>
</main>
)}
<Footer />
<Toaster />
</div>
)
}
export default DefaultLayout
TodoList Page Routing Definition
Finally, configure which routes file to route to when opening the screen. Here, create src/routes.ts
and configure requests to /todolist
to be routed to src/routes/todolist.tsx
.
import { layout, type RouteConfig, route } from '@react-router/dev/routes'
const PREFIX = './routes'
export default [
// biome-ignore format: for better readability
layout('./layouts/default.tsx', [
// Welcome page
route('/', `${PREFIX}/welcome.tsx`),
// Todo list page
route('/todolist', `${PREFIX}/todolist.tsx`),
]),
] satisfies RouteConfig
Functionality Testing
Once you've progressed this far, launch the application in development mode and perform functionality testing. Proceed to local execution debugging and testing.