TanStack Query for React Native data fetching. Use when implementing server state.
Manages server state in React Native with TanStack Query. Use when fetching API data, caching, or handling mutations with optimistic updates.
/plugin marketplace add IvanTorresEdge/molcajete.ai/plugin install react-native@Molcajete.aiThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers TanStack Query for React Native apps.
Use this skill when:
SERVER STATE - TanStack Query manages server state. Use Zustand for client state.
npm install @tanstack/react-query
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 2,
refetchOnWindowFocus: false, // Disable for mobile
},
},
});
export default function RootLayout(): React.ReactElement {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
);
}
import { useQuery } from '@tanstack/react-query';
interface User {
id: string;
name: string;
email: string;
}
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
function UserProfile({ userId }: { userId: string }): React.ReactElement {
const { data: user, isLoading, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) {
return <ActivityIndicator />;
}
if (isError) {
return <Text>Error: {error.message}</Text>;
}
return (
<View>
<Text>{user.name}</Text>
<Text>{user.email}</Text>
</View>
);
}
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/store/authStore';
function useUserPosts() {
const token = useAuthStore((state) => state.token);
return useQuery({
queryKey: ['posts', 'user'],
queryFn: async () => {
const response = await fetch('/api/posts', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) throw new Error('Failed to fetch posts');
return response.json();
},
enabled: !!token, // Only fetch when authenticated
});
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface CreatePostInput {
title: string;
content: string;
}
function useCreatePost() {
const queryClient = useQueryClient();
const token = useAuthStore((state) => state.token);
return useMutation({
mutationFn: async (input: CreatePostInput) => {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(input),
});
if (!response.ok) throw new Error('Failed to create post');
return response.json();
},
onSuccess: () => {
// Invalidate posts query to refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
// Usage
function CreatePostForm(): React.ReactElement {
const { mutate, isPending } = useCreatePost();
const handleSubmit = (data: CreatePostInput) => {
mutate(data, {
onSuccess: () => {
// Navigate or show success
},
onError: (error) => {
// Show error toast
},
});
};
return (
<Button onPress={() => handleSubmit({ title, content })} disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</Button>
);
}
function useLikePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (postId: string) =>
fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
onMutate: async (postId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Snapshot previous value
const previousPosts = queryClient.getQueryData(['posts']);
// Optimistically update
queryClient.setQueryData(['posts'], (old: Post[]) =>
old.map((post) =>
post.id === postId
? { ...post, likes: post.likes + 1, isLiked: true }
: post
)
);
return { previousPosts };
},
onError: (err, postId, context) => {
// Rollback on error
queryClient.setQueryData(['posts'], context?.previousPosts);
},
onSettled: () => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
import { useInfiniteQuery } from '@tanstack/react-query';
import { FlashList } from '@shopify/flash-list';
interface PostsResponse {
posts: Post[];
nextCursor: string | null;
}
function useInfinitePosts() {
return useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: async ({ pageParam }) => {
const response = await fetch(
`/api/posts?cursor=${pageParam ?? ''}&limit=20`
);
return response.json() as Promise<PostsResponse>;
},
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
}
function PostsList(): React.ReactElement {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
const posts = data?.pages.flatMap((page) => page.posts) ?? [];
return (
<FlashList
data={posts}
renderItem={({ item }) => <PostCard post={item} />}
estimatedItemSize={100}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}}
onEndReachedThreshold={0.5}
ListFooterComponent={
isFetchingNextPage ? <ActivityIndicator /> : null
}
/>
);
}
import { RefreshControl } from 'react-native';
function PostsList(): React.ReactElement {
const { data, isLoading, refetch, isRefetching } = usePosts();
return (
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={100}
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
/>
}
/>
);
}
// hooks/queries/usePosts.ts
export function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
}
export function usePost(id: string) {
return useQuery({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
enabled: !!id,
});
}
// hooks/mutations/usePostMutations.ts
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
export function useDeletePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deletePost,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
// lib/api.ts
import axios from 'axios';
import * as SecureStore from 'expo-secure-store';
const api = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL,
});
api.interceptors.request.use(async (config) => {
const token = await SecureStore.getItemAsync('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default api;
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.