In real projects, properly organizing and managing React Query queries is crucial for code maintainability and scalability:
Project Structure Organization
-
Separate Query Functions
- Separate data fetching logic from components
- Create dedicated API or service layer
- Example structure:
shell
src/ ├── api/ │ ├── index.js │ ├── users.js │ └── posts.js ├── components/ └── pages/
-
Custom Hook Encapsulation
- Create custom hooks to encapsulate common query logic
- Provide unified interfaces and configurations
- Example:
javascript
// src/hooks/useUsers.js import { useQuery } from 'react-query'; import { fetchUsers } from '../api/users'; export const useUsers = (options = {}) => { return useQuery('users', fetchUsers, { staleTime: 5 * 60 * 1000, ...options, }); };
-
Query Key Management
- Create centralized query key constants or utility functions
- Ensure consistency and maintainability of query keys
- Example:
javascript
// src/utils/queryKeys.js export const queryKeys = { users: 'users', user: (id) => ['user', id], posts: 'posts', userPosts: (userId) => ['posts', 'user', userId], };
Naming Conventions
-
Query Key Naming
- Use descriptive names
- For dynamic parameters, use array format
- Follow consistent naming patterns (e.g.,
[resource, id, action])
-
Custom Hook Naming
- Use
useprefix - Names should reflect the purpose of the query
- Examples:
useUsers,useUserPosts,useCreateUser
- Use
-
API Function Naming
- Use clear verb+noun structure
- Examples:
fetchUsers,createUser,updatePost
Best Practices
-
Global Configuration
- Configure QueryClient at application entry
- Set reasonable default options
- Example:
javascript
// src/App.js import { QueryClient, QueryClientProvider } from 'react-query'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30000, cacheTime: 60000, retry: 2, }, }, }); function App() { return ( <QueryClientProvider client={queryClient}> {/* Application components */} </QueryClientProvider> ); }
-
Query Grouping and Hierarchy
- Use hierarchical query keys
- Facilitate batch operations and invalidation
- Example:
javascript
// Hierarchical query keys const userQueryKey = ['users', userId]; const userPostsQueryKey = ['users', userId, 'posts']; // Batch invalidation queryClient.invalidateQueries(['users', userId]);
-
Code Splitting and Lazy Loading
- For large applications, consider code splitting
- Load query logic on demand
-
Testing Strategy
- Mock QueryClient and query responses
- Test component behavior in different query states
- Example:
javascript
// Using React Testing Library import { render, screen } from '@testing-library/react'; import { QueryClient, QueryClientProvider, useQuery } from 'react-query'; test('renders data when query succeeds', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity, }, }, }); // Pre-populate cache queryClient.setQueryData('todos', [{ id: 1, title: 'Test Todo' }]); render( <QueryClientProvider client={queryClient}> <TodoList /> </QueryClientProvider> ); expect(await screen.findByText('Test Todo')).toBeInTheDocument(); });
-
Documentation and Comments
- Add comments for complex queries
- Document query purposes, cache strategies, and dependencies
By following these best practices, you can create a more structured, maintainable, and scalable React Query codebase, improving development efficiency and code quality.