Skip to content

Fetching data the hard way (so the easy way makes sense) ​

You can load data with useEffect + useState. You will also reinvent caching, races, and retries. I've done it. You've probably done it in Vue too.

The minimal pattern ​

tsx
type Todo = { id: number; title: string }

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([])
  const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle')

  useEffect(() => {
    const controller = new AbortController()
    setStatus('loading')

    fetch('/api/todos', { signal: controller.signal })
      .then((r) => {
        if (!r.ok) throw new Error('bad status')
        return r.json()
      })
      .then((data: Todo[]) => {
        setTodos(data)
        setStatus('idle')
      })
      .catch((err) => {
        if (err.name === 'AbortError') return
        setStatus('error')
      })

    return () => controller.abort()
  }, [])

  if (status === 'loading') return <p>Loading…</p>
  if (status === 'error') return <p>Could not load.</p>

  return (
    <ul>
      {todos.map((t) => (
        <li key={t.id}>{t.title}</li>
      ))}
    </ul>
  )
}

Comments for the code above:

  • Abort on unmount / re-run - avoids setting state after goodbye
  • Three pieces of state already (data, status, and soon: pagination, filters…)
  • No shared cache - open the same page twice, fetch twice

What gets messy fast ​

  • Deduping identical requests across components
  • Stale data after a mutation (create todo → list still old)
  • Retries / backoff
  • Keeping "is fetching" vs "is showing cached data" straight
  • Prefetching on hover / route

In Vue land you might have stuffed some of this into Pinia. Same trap - a client store is not a server-state cache.

Where I'd put what ​

Kind of stateTool
Is the sidebar open?useState / Zustand
Who is logged in? (client session)Context / Zustand
List of todos from the APITanStack Query (next chapter)
Form draft before submituseState

As you can see - splitting server state from client state is the unlock. Next chapters: Zustand for client global state, TanStack Query for server cache.

Best,

-- ł.