react-query-manager
v2.2.0
Published
This is a library to simplify the work with @tanstack/react-query. It offers unified style for keys in the cache, ability to cancel a request. automatic cache refresh after mutation.
Downloads
403
Maintainers
Readme
react-query-manager is a library to simplify the work with @tanstack/react-query. It offers custom query hooks, automatically cleanses the cache after mutations, and provides a unified style for keys in the cache to make data management easier.
Purpose of the library
When working with projects that use @tanstack/react-query, the following issues often arise:
- Different format of cache keys: Cache keys can be created in different formats, which makes it difficult to find, update, and delete them.
- Cache clearing after mutations: After performing mutations, you must manually clear or update the corresponding cache entries to avoid inconsistent data.
What problems does the library solve?
react-query-manager
solves these problems by providing the following functionality:
- Unified key style: Provides a unified style for generating keys in the @tanstack/react-query cache, making them easier to find, update, and delete.
- Automatic cache refresh after mutation: The data in the cache is automatically updated immediately after a successful mutation, ensuring that the data in your application is instantly up to date.
- The ability to cancel a request: After performing a mutation, it is possible to cancel the last request within a few seconds, returning the cache to its original state. This is especially useful for cases when the user has performed an action by mistake and wants to quickly undo it.
- Automatic cache flushing: After mutations are performed, the corresponding cache entries are automatically cleared to ensure data consistency.
Install
npm install react-query-manager
Usage
App
import React from 'react';
import {
ToastBar, RQWrapper, ToastCustomContent,
ToastCustomUndoContent, toast,
} from 'react-query-manager';
import List from './List';
const CustomContent: ToastCustomContent = (toastProps) => {
return (
<ToastBar toast={toastProps} position={toastProps.position}>
{({ icon, message }) => {
return (
<>
{icon}
{message}
<svg
style={{
position: 'absolute',
top: 0,
right: 0,
cursor: 'pointer',
}}
viewBox="0 0 24 24"
width="15"
height="15"
onClick={() => {
toast.dismiss(toastProps.id);
}}
>
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
</>
);
}}
</ToastBar>
);
};
const CustomUndoContent: ToastCustomUndoContent = ({ message, type, onUndo }) => {
const texts = {
'delete-many': 'Cancel delete-many',
'delete-one': 'Cancel delete-one',
'update-many': 'Cancel update-many',
'update-one': 'Cancel update-one',
};
return (
<div>
<span>{message}</span>
<button type="button" onClick={onUndo}>{texts[type] || 'Undo'}</button>
</div>
);
};
export default function App() {
return (
<RQWrapper
isDevTools
devToolsOptions={{
buttonPosition: 'bottom-left',
}}
apiUrl="https://jsonplaceholder.typicode.com"
apiAuthorization={() => 'Bearer 12345'}
apiOnSuccess={(...args) => {
console.log('apiOnSuccess: ', args);
}}
apiOnError={(...args) => {
console.log('apiOnError: ', args);
}}
toast={{
globalProps: {
position: 'bottom-center',
},
CustomContent,
CustomUndoContent,
}}
>
<List />
</RQWrapper>
);
}
List
import React, { useState } from 'react';
import {
useGetList, useGetOne, useGetInfiniteList,
useUpdateMany, useUpdateOne,
useDeleteMany, useDeleteOne,
} from 'react-query-manager';
const API_POSTS_RESOURCE_PATH = 'posts';
type Post = {
userId: string;
id: string;
title: string;
body: string;
}
export default function List() {
const [selectedPostId, setSelectedPostId] = useState<string>('');
const queryPosts = useGetList<typeof API_POSTS_RESOURCE_PATH, Post>({
resource: { path: API_POSTS_RESOURCE_PATH, params: {} },
params: { _page: 1, _limit: 5 },
});
const queryPost = useGetOne<typeof API_POSTS_RESOURCE_PATH, Post>({
resource: { path: API_POSTS_RESOURCE_PATH, params: {} },
id: selectedPostId,
queryOptions: { enabled: !!selectedPostId },
});
const updatePost = useUpdateOne<typeof API_POSTS_RESOURCE_PATH, Post>({
resourcePath: API_POSTS_RESOURCE_PATH,
});
const updatePosts = useUpdateMany<typeof API_POSTS_RESOURCE_PATH, Post>({
resourcePath: API_POSTS_RESOURCE_PATH,
});
const deletePost = useDeleteOne<typeof API_POSTS_RESOURCE_PATH, Post>({
resourcePath: API_POSTS_RESOURCE_PATH,
});
const deletePosts = useDeleteMany<typeof API_POSTS_RESOURCE_PATH, Post>({
resourcePath: API_POSTS_RESOURCE_PATH,
});
const infiniteQueryPosts = useGetInfiniteList<typeof API_POSTS_RESOURCE_PATH, Post>({
resource: { path: API_POSTS_RESOURCE_PATH, params: {} },
pagination: { page: ['_page'], per_page: ['_limit', 10] },
});
return (
<div
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}
>
{queryPosts.isLoading ? (
<div>Loading List...</div>
) : (
<div>
<button
type="button"
onClick={() => {
updatePosts.update({
resourceParams: {},
ids: [1, 2, 3],
data: { title: 'Upd many title', body: 'Upd many body' },
});
}}
>
Update Many
</button>
<span style={{ display: 'inline-block', width: '15px' }} />
<button
type="button"
onClick={() => {
deletePosts.delete({
ids: [1, 2, 3],
resourceParams: {},
});
}}
>
Delete Many
</button>
{queryPosts?.data?.data?.map((post) => (
<div
style={{
background: 'purple',
padding: '5px',
margin: '10px 0',
color: 'white',
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
key={post.id}
onClick={() => {
setSelectedPostId(post.id);
}}
role="button"
tabIndex={0}
onKeyPress={() => setSelectedPostId(post.id)}
>
{post.title}
<button
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
deletePost.delete({
id: post.id,
resourceParams: {},
});
}}
>
Delete
</button>
</div>
))}
<div>
<h1>Selected Post</h1>
{queryPost.isLoading ? (
<div>Loading...</div>
) : (
<div>
{queryPost.data && (
<form
onSubmit={(event) => {
event.preventDefault();
const form = event.target as HTMLFormElement;
const title = (form.elements[0] as HTMLInputElement).value;
const body = (form.elements[1] as HTMLInputElement).value;
updatePost.update({
resourceParams: {},
id: selectedPostId,
data: { title, body },
});
}}
>
<input
key={queryPost.data?.data.title}
defaultValue={queryPost.data?.data.title}
style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
/>
<br />
<br />
<input
key={queryPost.data?.data.body}
defaultValue={queryPost.data?.data.body}
style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
/>
<br />
<br />
<button
style={{ width: '100%', padding: '10px', boxSizing: 'border-box' }}
type="submit"
>
Update
</button>
</form>
)}
</div>
)}
</div>
</div>
)}
{infiniteQueryPosts.isLoading ? (
<div>Loading Infinite List...</div>
) : (
<div>
<button
type="button"
onClick={() => {
infiniteQueryPosts.fetchNextPage();
}}
disabled={infiniteQueryPosts.isFetching}
>
{infiniteQueryPosts.isFetching ? 'Loading...' : 'next'}
</button>
<div
style={{
maxHeight: '420px',
overflow: 'auto',
}}
>
{infiniteQueryPosts?.data?.pages.map((page) => (
page.data.map((post) => (
<div
key={post.id}
style={{
background: 'purple',
padding: '5px',
margin: '10px 0',
color: 'white',
cursor: 'pointer',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{post.title}
<button
type="button"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
deletePost.delete({
id: post.id,
resourceParams: {},
});
}}
>
Delete
</button>
</div>
))
))}
</div>
</div>
)}
</div>
);
}