📄 tanstack/query/v4/docs/framework/react/examples/offline

File: offline.md | Updated: 11/15/2025

Source: https://tanstack.com/query/v4/docs/framework/react/examples/offline



TanStack

Query v4v4

Search...

+ K

Auto

Log In

TanStack StartRC

Docs Examples GitHub Contributors

TanStack Router

Docs Examples GitHub Contributors

TanStack Query

Docs Examples GitHub Contributors

TanStack Table

Docs Examples Github Contributors

TanStack Formnew

Docs Examples Github Contributors

TanStack DBbeta

Docs Github Contributors

TanStack Virtual

Docs Examples Github Contributors

TanStack Paceralpha

Docs Examples Github Contributors

TanStack Storealpha

Docs Examples Github Contributors

TanStack Devtoolsalpha

Docs Github Contributors

More Libraries

Maintainers Partners Support Learn StatsBETA Discord Merch Blog GitHub Ethos Brand Guide

Documentation

Framework

React logo

React

Version

v4

Search...

+ K

Menu

Getting Started

Guides & Concepts

Community Resources

API Reference

ESLint

Plugins

Examples

Framework

React logo

React

Version

v4

Menu

Getting Started

Guides & Concepts

Community Resources

API Reference

ESLint

Plugins

Examples

React Example: Offline

Github StackBlitz CodeSandbox

===============================================================================================================================================================================================================================================================================================================================================================================================

Code ExplorerCode

Interactive SandboxSandbox

  • public

  • src

    • App.jsx file iconApp.jsx

    • api.js file iconapi.js

    • index.jsx file iconindex.jsx

    • movies.js file iconmovies.js

  • .eslintrc file icon.eslintrc

  • .gitignore file icon.gitignore

  • README.md file iconREADME.md

  • index.html file iconindex.html

  • package.json file iconpackage.json

jsx

import * as React from 'react'

import {
  useQuery,
  QueryClient,
  MutationCache,
  onlineManager,
  useIsRestoring,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import toast, { Toaster } from 'react-hot-toast'

import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import {
  Link,
  Outlet,
  ReactLocation,
  Router,
  useMatch,
} from '@tanstack/react-location'

import * as api from './api'
import { movieKeys, useMovie } from './movies'

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})

const location = new ReactLocation()

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // 24 hours
      staleTime: 2000,
      retry: 0,
    },
  },
  // configure global cache callbacks to show toast notifications
  mutationCache: new MutationCache({
    onSuccess: (data) => {
      toast.success(data.message)
    },
    onError: (error) => {
      toast.error(error.message)
    },
  }),
})

// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults(movieKeys.all(), {
  mutationFn: async ({ id, comment }) => {
    // to avoid clashes with our optimistic update when an offline mutation continues
    await queryClient.cancelQueries({ queryKey: movieKeys.detail(id) })
    return api.updateMovie(id, comment)
  },
})

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // resume mutations after initial restore from localStorage was successful
        queryClient.resumePausedMutations().then(() => {
          queryClient.invalidateQueries()
        })
      }}
    >
      <Movies />
      <ReactQueryDevtools initialIsOpen />
    </PersistQueryClientProvider>
  )
}

function Movies() {
  const isRestoring = useIsRestoring()
  return (
    <Router
      location={location}
      routes={[\
        {\
          path: '/',\
          element: <List />,\
        },\
        {\
          path: ':movieId',\
          element: <Detail />,\
          errorElement: <MovieError />,\
          loader: ({ params: { movieId } }) =>\
            queryClient.getQueryData(movieKeys.detail(movieId)) ??\
            // do not load if we are offline or hydrating because it returns a promise that is pending until we go online again\
            // we just let the Detail component handle it\
            (onlineManager.isOnline() && !isRestoring\
              ? queryClient.fetchQuery({\
                  queryKey: movieKeys.detail(movieId),\
                  queryFn: () => api.fetchMovie(movieId),\
                })\
              : undefined),\
        },\
      ]}
    >
      <Outlet />
      <Toaster />
    </Router>
  )
}

function List() {
  const moviesQuery = useQuery({
    queryKey: movieKeys.list(),
    queryFn: api.fetchMovies,
  })

  if (moviesQuery.isLoading && moviesQuery.isFetching) {
    return 'Loading...'
  }

  if (moviesQuery.data) {
    return (
      <div>
        <h1>Movies</h1>
        <p>
          Try to mock offline behaviour with the button in the devtools. You can
          navigate around as long as there is already data in the cache. You'll
          get a refetch as soon as you go online again.
        </p>
        <ul>
          {moviesQuery.data.movies.map((movie) => (
            <li key={movie.id}>
              <Link to={`./${movie.id}`} preload>
                {movie.title}
              </Link>
            </li>
          ))}
        </ul>
        <div>
          Updated at: {new Date(moviesQuery.data.ts).toLocaleTimeString()}
        </div>
        <div>{moviesQuery.isFetching && 'fetching...'}</div>
      </div>
    )
  }

  // query will be in 'idle' fetchStatus while restoring from localStorage
  return null
}

function MovieError() {
  const { error } = useMatch()

  return (
    <div>
      <Link to="..">Back</Link>
      <h1>Couldn't load movie!</h1>
      <div>{error.message}</div>
    </div>
  )
}

function Detail() {
  const {
    params: { movieId },
  } = useMatch()
  const { comment, setComment, updateMovie, movieQuery } = useMovie(movieId)

  if (movieQuery.isLoading && movieQuery.isFetching) {
    return 'Loading...'
  }

  function submitForm(event) {
    event.preventDefault()

    updateMovie.mutate({
      id: movieId,
      comment,
    })
  }

  if (movieQuery.data) {
    return (
      <form onSubmit={submitForm}>
        <Link to="..">Back</Link>
        <h1>Movie: {movieQuery.data.movie.title}</h1>
        <p>
          Try to mock offline behaviour with the button in the devtools, then
          update the comment. The optimistic update will succeed, but the actual
          mutation will be paused and resumed once you go online again.
        </p>
        <p>
          You can also reload the page, which will make the persisted mutation
          resume, as you will be online again when you "come back".
        </p>
        <p>
          <label>
            Comment: <br />
            <textarea
              name="comment"
              value={comment}
              onChange={(event) => setComment(event.target.value)}
            />
          </label>
        </p>
        <button type="submit">Submit</button>
        <div>
          Updated at: {new Date(movieQuery.data.ts).toLocaleTimeString()}
        </div>
        <div>{movieQuery.isFetching && 'fetching...'}</div>
        <div>
          {updateMovie.isPaused
            ? 'mutation paused - offline'
            : updateMovie.isLoading && 'updating...'}
        </div>
      </form>
    )
  }

  if (movieQuery.isPaused) {
    return "We're offline and have no data to show :("
  }

  return null
}


import * as React from 'react'

import {
  useQuery,
  QueryClient,
  MutationCache,
  onlineManager,
  useIsRestoring,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import toast, { Toaster } from 'react-hot-toast'

import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import {
  Link,
  Outlet,
  ReactLocation,
  Router,
  useMatch,
} from '@tanstack/react-location'

import * as api from './api'
import { movieKeys, useMovie } from './movies'

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})

const location = new ReactLocation()

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // 24 hours
      staleTime: 2000,
      retry: 0,
    },
  },
  // configure global cache callbacks to show toast notifications
  mutationCache: new MutationCache({
    onSuccess: (data) => {
      toast.success(data.message)
    },
    onError: (error) => {
      toast.error(error.message)
    },
  }),
})

// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults(movieKeys.all(), {
  mutationFn: async ({ id, comment }) => {
    // to avoid clashes with our optimistic update when an offline mutation continues
    await queryClient.cancelQueries({ queryKey: movieKeys.detail(id) })
    return api.updateMovie(id, comment)
  },
})

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // resume mutations after initial restore from localStorage was successful
        queryClient.resumePausedMutations().then(() => {
          queryClient.invalidateQueries()
        })
      }}
    >
      <Movies />
      <ReactQueryDevtools initialIsOpen />
    </PersistQueryClientProvider>
  )
}

function Movies() {
  const isRestoring = useIsRestoring()
  return (
    <Router
      location={location}
      routes={[\
        {\
          path: '/',\
          element: <List />,\
        },\
        {\
          path: ':movieId',\
          element: <Detail />,\
          errorElement: <MovieError />,\
          loader: ({ params: { movieId } }) =>\
            queryClient.getQueryData(movieKeys.detail(movieId)) ??\
            // do not load if we are offline or hydrating because it returns a promise that is pending until we go online again\
            // we just let the Detail component handle it\
            (onlineManager.isOnline() && !isRestoring\
              ? queryClient.fetchQuery({\
                  queryKey: movieKeys.detail(movieId),\
                  queryFn: () => api.fetchMovie(movieId),\
                })\
              : undefined),\
        },\
      ]}
    >
      <Outlet />
      <Toaster />
    </Router>
  )
}

function List() {
  const moviesQuery = useQuery({
    queryKey: movieKeys.list(),
    queryFn: api.fetchMovies,
  })

  if (moviesQuery.isLoading && moviesQuery.isFetching) {
    return 'Loading...'
  }

  if (moviesQuery.data) {
    return (
      <div>
        <h1>Movies</h1>
        <p>
          Try to mock offline behaviour with the button in the devtools. You can
          navigate around as long as there is already data in the cache. You'll
          get a refetch as soon as you go online again.
        </p>
        <ul>
          {moviesQuery.data.movies.map((movie) => (
            <li key={movie.id}>
              <Link to={`./${movie.id}`} preload>
                {movie.title}
              </Link>
            </li>
          ))}
        </ul>
        <div>
          Updated at: {new Date(moviesQuery.data.ts).toLocaleTimeString()}
        </div>
        <div>{moviesQuery.isFetching && 'fetching...'}</div>
      </div>
    )
  }

  // query will be in 'idle' fetchStatus while restoring from localStorage
  return null
}

function MovieError() {
  const { error } = useMatch()

  return (
    <div>
      <Link to="..">Back</Link>
      <h1>Couldn't load movie!</h1>
      <div>{error.message}</div>
    </div>
  )
}

function Detail() {
  const {
    params: { movieId },
  } = useMatch()
  const { comment, setComment, updateMovie, movieQuery } = useMovie(movieId)

  if (movieQuery.isLoading && movieQuery.isFetching) {
    return 'Loading...'
  }

  function submitForm(event) {
    event.preventDefault()

    updateMovie.mutate({
      id: movieId,
      comment,
    })
  }

  if (movieQuery.data) {
    return (
      <form onSubmit={submitForm}>
        <Link to="..">Back</Link>
        <h1>Movie: {movieQuery.data.movie.title}</h1>
        <p>
          Try to mock offline behaviour with the button in the devtools, then
          update the comment. The optimistic update will succeed, but the actual
          mutation will be paused and resumed once you go online again.
        </p>
        <p>
          You can also reload the page, which will make the persisted mutation
          resume, as you will be online again when you "come back".
        </p>
        <p>
          <label>
            Comment: <br />
            <textarea
              name="comment"
              value={comment}
              onChange={(event) => setComment(event.target.value)}
            />
          </label>
        </p>
        <button type="submit">Submit</button>
        <div>
          Updated at: {new Date(movieQuery.data.ts).toLocaleTimeString()}
        </div>
        <div>{movieQuery.isFetching && 'fetching...'}</div>
        <div>
          {updateMovie.isPaused
            ? 'mutation paused - offline'
            : updateMovie.isLoading && 'updating...'}
        </div>
      </form>
    )
  }

  if (movieQuery.isPaused) {
    return "We're offline and have no data to show :("
  }

  return null
}

React Router

Algolia

Partners Become a Partner

Code RabbitCode Rabbit CloudflareCloudflare AG GridAG Grid NetlifyNetlify NeonNeon WorkOSWorkOS ClerkClerk ConvexConvex ElectricElectric SentrySentry PrismaPrisma StrapiStrapi UnkeyUnkey

[###### Want to Skip the Docs?

Query.gg - The Official React Query Course
\

“If you’re serious about *really* understanding React Query, there’s no better way than with query.gg”—Tanner Linsley

Learn More](https://query.gg/?s=tanstack)

You are currently reading v4 docs. Redirect to latest version?

Latest Hide

scarf analytics