> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/tanstack/query/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native

> Use TanStack Query in React Native applications

# React Native

TanStack Query works seamlessly with React Native, providing the same powerful data synchronization features for mobile apps.

## Installation

Install React Query in your React Native project:

<CodeGroup>
  ```bash npm theme={null}
  npm install @tanstack/react-query
  ```

  ```bash yarn theme={null}
  yarn add @tanstack/react-query
  ```

  ```bash pnpm theme={null}
  pnpm add @tanstack/react-query
  ```
</CodeGroup>

<Note>
  React Query supports React Native 0.70+ and works with Expo, bare React Native, and React Native Web.
</Note>

## Setup

Configure React Query in your React Native app:

```tsx App.tsx theme={null}
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { StatusBar } from 'expo-status-bar'
import { View } from 'react-native'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 2,
      staleTime: 60 * 1000, // 1 minute
    },
  },
})

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <View style={{ flex: 1 }}>
        <StatusBar style="auto" />
        <YourApp />
      </View>
    </QueryClientProvider>
  )
}
```

## Basic Usage

Use React Query hooks just like in React web apps:

```tsx screens/PostsScreen.tsx theme={null}
import { useQuery } from '@tanstack/react-query'
import {
  View,
  Text,
  FlatList,
  ActivityIndicator,
  StyleSheet,
} from 'react-native'

interface Post {
  id: number
  title: string
  body: string
}

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts')
  if (!response.ok) {
    throw new Error('Network response was not ok')
  }
  return response.json()
}

export function PostsScreen() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  if (isLoading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" />
      </View>
    )
  }

  if (error) {
    return (
      <View style={styles.centered}>
        <Text>Error: {error.message}</Text>
      </View>
    )
  }

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => (
        <View style={styles.post}>
          <Text style={styles.title}>{item.title}</Text>
          <Text>{item.body}</Text>
        </View>
      )}
      onRefresh={refetch}
      refreshing={isLoading}
    />
  )
}

const styles = StyleSheet.create({
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  post: {
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#eee',
  },
  title: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 8,
  },
})
```

## Pull-to-Refresh

Integrate with React Native's pull-to-refresh:

```tsx theme={null}
import { useQuery } from '@tanstack/react-query'
import { FlatList, RefreshControl } from 'react-native'

function PostsList() {
  const { data, isLoading, isFetching, refetch } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => <PostItem item={item} />}
      refreshControl={
        <RefreshControl
          refreshing={isFetching}
          onRefresh={refetch}
        />
      }
    />
  )
}
```

<Tip>
  Use `isFetching` instead of `isLoading` for the refresh indicator to show activity during background refetches.
</Tip>

## Infinite Scroll

Implement infinite scrolling with `useInfiniteQuery`:

```tsx theme={null}
import { useInfiniteQuery } from '@tanstack/react-query'
import { FlatList, ActivityIndicator, View } from 'react-native'

interface PostsPage {
  posts: Post[]
  nextPage: number | null
}

async function fetchPosts({ pageParam = 1 }): Promise<PostsPage> {
  const response = await fetch(
    `https://api.example.com/posts?page=${pageParam}&limit=20`
  )
  const posts = await response.json()
  return {
    posts,
    nextPage: posts.length === 20 ? pageParam + 1 : null,
  }
}

export function InfinitePostsList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    initialPageParam: 1,
    getNextPageParam: (lastPage) => lastPage.nextPage,
  })

  if (isLoading) {
    return <ActivityIndicator />
  }

  const posts = data?.pages.flatMap((page) => page.posts) ?? []

  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id.toString()}
      renderItem={({ item }) => <PostItem item={item} />}
      onEndReached={() => {
        if (hasNextPage && !isFetchingNextPage) {
          fetchNextPage()
        }
      }}
      onEndReachedThreshold={0.5}
      ListFooterComponent={
        isFetchingNextPage ? <ActivityIndicator /> : null
      }
    />
  )
}
```

## Mutations

Handle create, update, and delete operations:

```tsx theme={null}
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import {
  View,
  TextInput,
  TouchableOpacity,
  Text,
  StyleSheet,
  Alert,
} from 'react-native'

interface CreatePostInput {
  title: string
  body: string
}

async function createPost(input: CreatePostInput): Promise<Post> {
  const response = await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(input),
  })
  return response.json()
}

export function CreatePostForm() {
  const [title, setTitle] = useState('')
  const [body, setBody] = useState('')
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] })
      setTitle('')
      setBody('')
      Alert.alert('Success', 'Post created successfully!')
    },
    onError: (error: Error) => {
      Alert.alert('Error', error.message)
    },
  })

  return (
    <View style={styles.form}>
      <TextInput
        style={styles.input}
        placeholder="Title"
        value={title}
        onChangeText={setTitle}
      />
      <TextInput
        style={[styles.input, styles.textArea]}
        placeholder="Body"
        value={body}
        onChangeText={setBody}
        multiline
        numberOfLines={4}
      />
      <TouchableOpacity
        style={[
          styles.button,
          mutation.isPending && styles.buttonDisabled,
        ]}
        onPress={() => mutation.mutate({ title, body })}
        disabled={mutation.isPending}
      >
        <Text style={styles.buttonText}>
          {mutation.isPending ? 'Creating...' : 'Create Post'}
        </Text>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  form: {
    padding: 16,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    padding: 12,
    marginBottom: 12,
    fontSize: 16,
  },
  textArea: {
    height: 100,
    textAlignVertical: 'top',
  },
  button: {
    backgroundColor: '#007AFF',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonDisabled: {
    opacity: 0.6,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
})
```

## App State Management

React Query automatically refetches when the app comes to foreground:

```tsx theme={null}
import { QueryClient } from '@tanstack/react-query'
import { AppState } from 'react-native'
import { focusManager } from '@tanstack/react-query'
import { useEffect } from 'react'

// Configure focus manager for React Native
function onAppStateChange(status: string) {
  focusManager.setFocused(status === 'active')
}

export function useAppState() {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', onAppStateChange)
    return () => subscription.remove()
  }, [])
}

// Use in your app
function App() {
  useAppState()
  
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  )
}
```

<Note>
  React Query's `focusManager` integrates with React Native's AppState to automatically refetch when users return to your app.
</Note>

## Network State Management

Handle online/offline states:

```tsx theme={null}
import { onlineManager } from '@tanstack/react-query'
import NetInfo from '@react-native-community/netinfo'
import { useEffect } from 'react'

export function useNetworkState() {
  useEffect(() => {
    return NetInfo.addEventListener((state) => {
      onlineManager.setOnline(
        state.isConnected != null &&
        state.isConnected &&
        Boolean(state.isInternetReachable)
      )
    })
  }, [])
}

// Use in your app
function App() {
  useNetworkState()
  
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  )
}
```

Install the required dependency:

```bash theme={null}
npm install @react-native-community/netinfo
```

## Persistence

Persist query cache to AsyncStorage:

```bash theme={null}
npm install @tanstack/react-query-persist-client
npm install @react-native-async-storage/async-storage
```

```tsx theme={null}
import { QueryClient } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import AsyncStorage from '@react-native-async-storage/async-storage'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24, // 24 hours
    },
  },
})

const persister = createAsyncStoragePersister({
  storage: AsyncStorage,
})

function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
    >
      <YourApp />
    </PersistQueryClientProvider>
  )
}
```

<Warning>
  Be mindful of AsyncStorage size limits. Consider implementing cache size limits or selective persistence for production apps.
</Warning>

## Optimistic Updates

Provide instant feedback in mobile apps:

```tsx theme={null}
import { useMutation, useQueryClient } from '@tanstack/react-query'

function useLikeMutation() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (postId: number) => likePost(postId),
    onMutate: async (postId) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['post', postId] })

      // Snapshot previous value
      const previousPost = queryClient.getQueryData(['post', postId])

      // Optimistically update
      queryClient.setQueryData(['post', postId], (old: any) => ({
        ...old,
        likes: old.likes + 1,
        isLiked: true,
      }))

      return { previousPost }
    },
    onError: (err, postId, context) => {
      // Rollback on error
      queryClient.setQueryData(['post', postId], context?.previousPost)
    },
    onSettled: (data, error, postId) => {
      queryClient.invalidateQueries({ queryKey: ['post', postId] })
    },
  })
}
```

## React Navigation Integration

Prefetch data when navigating:

```tsx theme={null}
import { useQueryClient } from '@tanstack/react-query'
import { useFocusEffect } from '@react-navigation/native'
import { useCallback } from 'react'

function PostDetailsScreen({ route }: { route: any }) {
  const { postId } = route.params
  const queryClient = useQueryClient()

  // Refetch on screen focus
  useFocusEffect(
    useCallback(() => {
      queryClient.invalidateQueries({ queryKey: ['post', postId] })
    }, [queryClient, postId])
  )

  const { data } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })

  return <PostDetails post={data} />
}

// Prefetch in list screen
function PostsListScreen({ navigation }: any) {
  const queryClient = useQueryClient()

  const handlePostPress = (postId: number) => {
    // Prefetch before navigation
    queryClient.prefetchQuery({
      queryKey: ['post', postId],
      queryFn: () => fetchPost(postId),
    })
    
    navigation.navigate('PostDetails', { postId })
  }

  return <PostsList onPostPress={handlePostPress} />
}
```

## Error Boundaries

Handle errors gracefully in React Native:

```tsx theme={null}
import { Component, ReactNode } from 'react'
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'

interface Props {
  children: ReactNode
}

interface State {
  hasError: boolean
  error: Error | null
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  render() {
    if (this.state.hasError) {
      return (
        <View style={styles.container}>
          <Text style={styles.title}>Something went wrong</Text>
          <Text style={styles.message}>
            {this.state.error?.message}
          </Text>
          <TouchableOpacity
            style={styles.button}
            onPress={() => this.setState({ hasError: false, error: null })}
          >
            <Text style={styles.buttonText}>Try Again</Text>
          </TouchableOpacity>
        </View>
      )
    }

    return this.props.children
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  message: {
    fontSize: 14,
    color: '#666',
    marginBottom: 20,
    textAlign: 'center',
  },
  button: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 8,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
  },
})

export default ErrorBoundary
```

## Performance Tips

<Steps>
  <Step title="Optimize re-renders">
    Use `notifyOnChangeProps` to limit updates:

    ```tsx theme={null}
    useQuery({
      queryKey: ['posts'],
      queryFn: fetchPosts,
      notifyOnChangeProps: ['data', 'error'],
    })
    ```
  </Step>

  <Step title="Implement pagination">
    Load data in chunks for better performance:

    ```tsx theme={null}
    useInfiniteQuery({
      queryKey: ['posts'],
      queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
      initialPageParam: 1,
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    })
    ```
  </Step>

  <Step title="Configure appropriate cache times">
    ```tsx theme={null}
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 60 * 1000, // 1 minute
          gcTime: 5 * 60 * 1000, // 5 minutes
        },
      },
    })
    ```
  </Step>
</Steps>

<Tip>
  Monitor query performance using React DevTools Profiler and React Query DevTools (web debugging).
</Tip>

## Next Steps

<CardGroup cols={2}>
  <Card title="TypeScript" icon="code" href="/frameworks/react/typescript">
    Add type safety to your React Native queries
  </Card>

  <Card title="GraphQL" icon="diagram-project" href="/frameworks/react/graphql">
    Use GraphQL in React Native apps
  </Card>
</CardGroup>
