DEV Community

Govind Shah
Govind Shah

Posted on

Building a Clean useTodos Hook (TanStack Query + Appwrite) — Explained from Scratch

Today I implemented a useTodos system in my project using Appwrite + TanStack Query, and I want to explain everything clearly — from fundamentals to real usage.

This is not just code — it’s about architecture and thinking like a developer.


🧠 First — Understand the Big Picture

Think of your app like a restaurant:

Customer (Component)
    ↓
Waiter (Hook)
    ↓
Kitchen (API layer)
    ↓
Database (Appwrite)
Enter fullscreen mode Exit fullscreen mode
  • Components never talk directly to the database
  • Hooks manage logic and state
  • API layer handles backend communication

👉 Each layer has one responsibility. This keeps your app scalable and clean.


📁 1. env.ts — Centralized Environment Config

const env = {
  appwriteUrl: String(import.meta.env.VITE_APPWRITE_URL),
  appwriteDatabaseId: String(import.meta.env.VITE_APPWRITE_DATABASE_ID),
  appwriteCollectionTodos: String(import.meta.env.VITE_APPWRITE_COLLECTION_TODOS),
}

export default env
Enter fullscreen mode Exit fullscreen mode

Key Concepts:

  • import.meta.env → Access .env variables in Vite
  • String(...) → Prevents undefined type issues
  • Central file → Avoid repetition and typos

✅ Cleaner code
✅ Easier maintenance


🔌 2. API Layer — todos.ts

This layer talks directly to Appwrite.

Fetch Todos

export const fetchTodos = async (userId: string): Promise<Todo[]> => {
  const response = await databases.listDocuments({
    databaseId: env.appwriteDatabaseId,
    collectionId: env.appwriteCollectionTodos,
    queries: [
      Query.equal("userId", userId),
      Query.orderDesc("$createdAt"),
      Query.limit(100),
    ],
  })

  return response.documents as unknown as Todo[]
}
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • Filters by userId → security
  • Sorted → newest first
  • Limited → performance optimized

Create Todo

export const createTodo = async (data: {
  title: string
  priority: "high" | "medium" | "low"
  userId: string
}) => {
  return databases.createDocument({
    databaseId: env.appwriteDatabaseId,
    collectionId: env.appwriteCollectionTodos,
    documentId: ID.unique(),
    data: {
      ...data,
      completed: false,
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

Concepts:

  • ID.unique() → auto ID generation
  • Union types → prevent invalid values
  • Default field → completed: false

Update Todo

export const updateTodo = async (id: string, data: Partial<Todo>) => {
  return databases.updateDocument({
    databaseId: env.appwriteDatabaseId,
    collectionId: env.appwriteCollectionTodos,
    documentId: id,
    data,
  })
}
Enter fullscreen mode Exit fullscreen mode

👉 Partial<Todo> = update only changed fields


Delete Todo

export const deleteTodo = async (id: string) => {
  await databases.deleteDocument({
    databaseId: env.appwriteDatabaseId,
    collectionId: env.appwriteCollectionTodos,
    documentId: id,
  })
}
Enter fullscreen mode Exit fullscreen mode

⚛️ 3. Hook Layer — useTodos.ts

This is where TanStack Query shines.


Query Key

const TODO_KEY = ["todos"]
Enter fullscreen mode Exit fullscreen mode

Think of this as a cache label.


useTodos — Fetch + Cache

export function useTodos() {
  const user = useAuthStore((state) => state.user)

  return useQuery({
    queryKey: TODO_KEY,
    queryFn: () => fetchTodos(user!.$id),
    enabled: !!user,
    staleTime: 1000 * 60 * 5,
  })
}
Enter fullscreen mode Exit fullscreen mode

Important:

  • enabled: !!user → don't fetch if not logged in
  • staleTime → cache for 5 minutes
  • user! → safe due to enabled check

useCreateTodo

export function useCreateTodo() {
  const queryClient = useQueryClient()
  const user = useAuthStore((state) => state.user)

  return useMutation({
    mutationFn: (newTodo) =>
      createTodo({ ...newTodo, userId: user!.$id }),

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: TODO_KEY })
      toast.success("Todo created!")
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

Why invalidate?

👉 It tells TanStack:

"Data changed — refetch it"


useToggleTodo

export function useToggleTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: ({ id, completed }) =>
      updateTodo(id, { completed }),

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: TODO_KEY })
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

👉 No success toast → better UX for checkboxes


useDeleteTodo

export function useDeleteTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (id: string) => deleteTodo(id),

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: TODO_KEY })
      toast.success("Todo deleted")
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

🧩 Real Usage in Pages

Dashboard (Read-only)

  • Show stats
  • No mutations

Todos Page

  • Read + toggle + delete

Create Page

  • Only mutation

⚡ Superpower of TanStack Query

If multiple components use:

useTodos()
Enter fullscreen mode Exit fullscreen mode

👉 Only ONE API request is made
👉 Data is shared via cache


📌 Key Concepts (Quick Summary)

Concept Meaning
API Layer Handles backend logic
Hook Layer Handles UI state + caching
queryKey Cache identifier
enabled Control when query runs
staleTime Cache duration
invalidateQueries Refetch updated data
Partial<T> Update only needed fields
mutate() Trigger mutation
isPending Loading state

🧠 What I Learned

  • Separation of concerns is everything
  • TanStack Query removes most manual state handling
  • Clean architecture makes scaling easy
  • Small decisions (like query keys) matter a lot

🤖 Bonus

Sometimes I also use Claude (free version) to:

  • Understand complex code
  • Break down logic
  • Plan project structure step-by-step

It helps speed up learning, but I always verify and implement myself.


🚀 Next Step

I’m now building:

👉 useArticles.ts using the same pattern


If you're learning React + backend integration, this pattern is a game changer.

Let’s connect and learn together 👨‍💻

Top comments (0)