- auto caching
- data are in sync across multiple Hooks.
- ability to combine the same multiple network requests (determined by
dataQueryKey
) into a single one. - ability to avoid race conditions.
- reducing the number of render times as much as possible.
- freshness of data are guaranteed (depends on passed
options
object). - can also be used to share global state as it has been built using callbacks and guarantee to re-render only affected components.
Client side asynchronous remote state management library. Library's syntaxes and functionalities are very similar to and inspired by react-query
library. RDQ makes network requests and cache the results along with the dataQueryKey
.
I know this library is decreasing popularity because I initially released using JavaScript. So people who are using this have to look back documentation to know how to use exactly. But from version
2.1.0
, you will get the helps of Intellisense.
I am not sure that this library is production-grade. So don't take it seriously.
1.0.0
and2.0.0
were deprecated. Because of unexpected Error.2.0.2
,2.0.3
and2.0.4
were unpublished. Because I made a small change and published it without testing. As the result, it causes the Error. I will try not to happen like that again.2.0.9
- Fixed setting data ofuseInfiniteDataQuery
viasetQueryData
andsetQueriesData
. Read API Reference ofuseInfiniteDataQuery
2.1.0
integrated with TypeScript.- From
2.1.9
, you can import TypeScript types. 2.2.0
- From this version, giving you the flexibilities to describe whatdata
type will be returned and how the shape of thecontext
will be look like. And more...2.3.0
- added new status 'isSuccess'
Please install the latest version
Coming soon!
npm install react-data-query
Please note that react-data-query
was built using react ^18.2.0
and react-dom with the same version.
Note: before you start using Hooks, you need to wrap your components with
DataQueryProvider
, which is a context provider for all Hooks. You should putDataQueryProvider
on top of all your components. For example :
import { DataQueryProvider } from "react-data-query";
export default function App(props) {
return <DataQueryProvider>{/* your components */}</DataQueryProvider>;
}
Now you can start using Hooks provided by react-data-query
library!
import { useDataQuery } from "react-data-query";
const fetchPets = (context) => {
return fetch("url").then((res) => res.json());
};
export const Pets = (props) => {
const queryInstance = useDataQuery(["pets"], fetchPets);
return <div>{/* some jsx elements */}</div>;
};
DataQueryProvider
is a context provider for all components in which they are using library's Hooks. Hooks that were not passed any options
by library users, will gains the default options
from this provider.
DataQueryProvider
accepts only two props called options
and children
. options
accepts only object
type. For example :
<DataQueryProvider options={{...}}>
Name | Type | Default | Description |
---|---|---|---|
cacheTime |
number |
1000 * 60 * 5 (5 mins) |
Set the cache time duration in milliseconds. If it has elapsed, the state data will then be set with empty data while the data is being fetched when the component mounts or on dataQueryKey changes. |
staleTime |
number |
1000 * 10 (10 secs) |
Set the stale time duration in milliseconds. This will be used to determine whether the data is stale or not. When staleTime has elapsed, the component will be notified that the data is stale, i.e the component will be re-rendered. If the staleTime doesn't elapse yet, fresh data that was caused by new network request of other Hooks with the same dataQueryKey won't be set to the current Hook. |
keepCacheAlways |
boolean |
false |
If set to true , the state data will always get from cache when initializing state. It takes precedence over cacheTime . |
keepValueOnKeyChanges |
boolean |
true |
When the dataQueryKey changes, the data with previous key will always be shown. This is useful if we want users to see the screen with previous data until the data for the next screen is ready. |
dataStayInSync |
boolean |
true |
Use to determine whether the hook want to stay in sync when the data was changed (due to new network request) with the same dataQueryKey . |
autoFetchEnabled |
boolean |
true |
Set to false if you want to manually fetch data. Typically use in dependent queries by toggling this property. |
refetchOnWindowFocus |
boolean |
true |
Set to false unless you want to fetch data when gains focus again after losing it. This can ensure that the app receives fresh data. |
markUpdatesAsTransitions |
boolean |
false |
Mark state updates as transition, i.e wrap state setter functions within the startTransition function. If the state update is not critical to the immediate rendering or user experience, you can consider setting to true . |
offsetBottom |
number |
0 |
Set any value that can be converted to number. This option is useful when you want to fetch data before the scroll position reaches the bottom. Use with useExperimentalInfiniteScrollDataQuery Hook. |
useDataQuery
, useDataMutation
, useInfiniteDataQuery
, useExperimentalInfiniteScrollDataQuery
, and useDataQueryMagic
// Type parameter `T` for the shape of returned `data`
useDataQuery<T>(dataQueryKey, fetcher?, options?)
// `MutatorInput` defines the shape of the argument passed to `mutate` and `onMutate` callback's parameter
// `ReturnedData` defines the shape of the returned data
// `Context` defines the shape of callbacks's parameter `onSuccess`, `onError`, and `onSettled`. `onMutate` callback should return the promise that resolve `Context` data type.
useDataMutation<MutatorInput, ReturnedData, Context>(mutator, callbacks?)
// Type parameter `T` for the shape of returned `data`. You DON'T NEED to define array type. Just need to define the shape of each item.
// For example, if you pass Color type argument, it will return like Color[][]
useInfiniteDataQuery<T>(dataQueryKey, fetcher, options);
// Same as `useInfiniteDataQuery` Hook.
useExperimentalInfiniteScrollDataQuery<T>(
dataQueryKey,
fetcher,
containerRef,
options
);
A Hook to make developer's life easier. This Hook will help you in fetching data and keep you away from handling caches. You don't need to implement imperative stuffs. You can also use this Hook to manage state. Other Hooks, except useDataMutation
, were built on top of this Hook.
useDataQuery(dataQueryKey, fetcher?, options?)
Parameters
dataQueryKey
- can be any type. Better avoid falsy values. This helps to keep the query in queue and cache. When this value changed, the new data has to be initialized, i.e the new network request will be initiated. You don't need to worry about network request duplications asreact-data-query
will internally handle it.fetcher
- the function who initiates network request. Must return promise that will be resolved by the result of network request completion. For example:
const fetcher = (context) => fetch('url`).then(res => res.json());
context
is an object that containsdataQueryKey
andparam
properties.param
can be of any type that is passed as the argument ofrefetch
function returned byuseDataQuery
. Note also that you should not try to catch network related error infetcher
function.useDataQuery
will notify you whenever the error occurs.
-
options
- to override theoptions
received fromDataQueryProvider
. You can also pass callback functions such asonSuccess
,onError
,onSettled
, andonMutated
.initialData
(v2.2.0) - initial data to return. It will be cached silently i.e other Hooks with samedataQueryKey
will not be notified when it is being cached. Please note that if the data with the samedataQueryKey
already exists in the cache, that cached data will return instead ofinitialData
.onSuccess(data)
- which will be invoked when network request successfully completed.onError(reason)
- which will be invoked when network related error has occurred. AllonError
callbacks passed to library Hooks will not automatically be invoked on Server Error (HTTP statuses like404
,500
). You must explicitly throw it in your fetcher function.
fetch(...).then(res => { if(!res.ok) throw new Error(...) return res.json(); })
onSettled(data, reason)
- which will be invoked whether or not succeeded or failed`.onMutated(data)
- which will be invoked when you invokesetQueryData
andsetQueriesData
functions returned fromuseDataQueryMagic
Hook.
Return Read only query instance object whose properties are :
Name | Type | Description |
---|---|---|
dataQueryKey |
any |
the first argument of useDataQuery Hook |
data |
any |
the value that resolved to the returned Promise of your fetcher function |
error |
any |
The reason why error occurred |
isLoading |
boolean |
Indicating whether the data is being fetched. true for only first time network request. But if you have disabled keepValueOnKeyChanges , this will be set to true if there is no cache for new dataQueryKey . |
isFetching |
boolean |
Indicating whether the data is being fetched. This will always be true on every network requests. |
isSuccess |
boolean |
Indicating whether the data is successfully fetched. |
isError |
boolean |
Indicating whether the network related error has occurred. |
isStale |
boolean |
Indicating whether the data becomes stale or not. The component will be re-rendered when the data becomes stale. |
refetch |
function |
A function to manually fetch data. When you call this function, the new network request will be initiated only if there is no already ongoing request with the same dataQueryKey . When the data becomes available, the Hook will be notified with new data and update the data state with that new data. |
forceRefetch |
function |
Unlike refetch function, this will cancel ongoing network request with the same dataQueryKey and initiate the new network request. |
Cancelling network request doesn't actually cancel the request. It is just protecting from race conditions. However, future releases will actually cancel the network request by using
AbortController
.
Parallel Data Query
const { data: users } = useDataQuery("users", fetcher, options);
const { data: pets } = useDataQuery("pets", fetcher, options);
Dependent Data Query Fetch channel after the user becomes available
const { data: user } = useDataQuery(`user`, fetcher, options);
const { data: channel } = useDataQuery(["channel", user?.id], fetcher, {
autoFetchEnabled: !!user?.id,
});
Paginated Data Query
You can fetch paginated data using this Hook. For example :
import { useState } from "react";
import { useDataQuery } from "react-data-query";
const fetcher = (context) => {
const { dataQueryKey } = context;
return fetch(`https://.../pets?page=${dataQueryKey[1]}`).then((res) =>
res.json()
);
};
// To keep previous value until the data for the next page is ready
const options = { keepValueOnKeyChanges: true };
export const Pets = (props) => {
const [page, setPage] = useState(0);
const { data: pets, isFetching } = useDataQuery(
[`pets`, page],
fetcher,
options
);
return (
<div>
{isFetching && <p class="status">Fetching...</p>}
<button onClick={() => setPage((p) => p + 1)}>Next</button>
<button onClick={() => setPage((p) => p - 1)}>Prev</button>
<div>
{pets?.map((pet) => {
// return your jsx
})}
</div>
</div>
);
};
Further explanations with examples will come soon!
A Hook that can be used to mutate remote data. Very similar to useMutation
Hook of react-query
.
useDataMutation(mutator, callbacks?)
Parameters
mutator
- a function to mutate remote data. For example :
const mutatePets = (newPet) => fetch(url, { method: 'POST', body: JSON.stringfy(newPet), ... }).then(res => res.json());
callbacks
- an object with a collection of callback functions.onMutate(newData)
- asynchronous function that will be invoked before actual mutation. Other callbacks accept context object returned from this function.onSuccess(mutatedData, context)
- will be invoked when the remote data has been mutated.onError(err, newData, context)
- will be invoked if mutation failed.onSettled(newData, err, context)
- will be invoked whether or not succeeded or failed.
Return an object with the set of properties :
Name | Type | Description |
---|---|---|
data |
any | null |
Data that represents mutated data |
error |
Error object |
The reason why error occurred |
isMutating |
boolean |
Indicating whether data is mutating |
isSuccess |
boolean |
Indicating whether remote data is successfully mutated |
isError |
boolean |
Indicating whether the error has occurred |
mutate |
function |
It accepts one argument. When you call this function, it invokes mutator function with its argument. Before actual mutation, it invokes onMutate callback function first with its argument. |
There is one example below of
useDataQueryMagic
's API reference.
Further explanations with examples will come soon!
A Hook that can be used to fetch data infinitely. Unlike other Hooks, will not synchronize data across multiple Hooks with the same dataQueryKey
. Syntaxes are very similar to useInfiniteQuery
Hook of react-query
.
useInfiniteDataQuery(dataQueryKey, fetcher, options);
Unlike
useDataQuery
, this Hook will not synchronize data between of this Hooks. If you use this Hook in two components with the samedataQueryKey
, unexpected behaviors will occur.
If you want to update data of this Hooks via
setQueryData
orsetQueriesData
, you should not passquerySetter
as a function. Instead, passarray
that represent a new page.
Parameters
dataQueryKey
- same asuseDataQuery
.fetcher
- function who initiate network request.
const fetcher = (context) => {
// extract pageParam from param
const { pageParam } = context.param;
return fetch(`https://...${pageParam}`).then((res) => res.json());
};
options
- to override defaultoptions
received fromDataQueryProvider
.autoFetchEnabled
andrefetchOnWindowFocus
are disabled. You can't modify it. There are another two properties you need to pass. They are callback function :getNextPageParam
andgetPrevPageParam
.onSuccess
,onError
andonSettled
are optional.getNextPageParam(lastPage, pages)
- This function will be used to determinepageParam
for the next page and for thehasNextPage
status. Returnundefined
if there is no more pages. Don't rely on the number of invoked times as it will generally be invoked three times for one page. Will fix it in future releases.getPrevPageParam(firstPage, pages)
- This function will be used to determinepageParam
for the previous page and for thehasPrevPage
status. Returnundefined
if there is no more pages. Don't rely on the number of invoked times as it will generally be invoked three times for one page. Will fix it in future releases.onReset(fetchPage
) - Will be invoked when you invokereset
function returned fromuseInfiniteDataQuery
. Read more aboutfetchPage
function below.
Return
an object with a set of properties:
Name | Type | Description |
---|---|---|
data |
array |
an array of array that represent pages |
error |
Error |
reason why error has occurred. |
isFetching |
boolean |
Indicating whether the data is being fetched. This will always be true on every network requests. |
isLoading |
boolean |
Indicating whether the data is being fetched. true for only first time network request. But if you have disabled keepValueOnKeyChanges , this will be set to true if there is no cache for new dataQueryKey . |
isFetchingNextPage |
boolean |
Indicating whether the data for the next page is being fetched. |
isFetchingPrevPage |
boolean |
Indicating whether the data for the previous page is being fetched. |
hasNextPage |
boolean |
Indicating whether the data for the next page exists. |
hasPrevPage |
boolean |
Indicating whether the data for the previous page exists. |
isError |
boolean |
Indicating whether the network related error has occurred. |
fetchPage |
function |
Fetch specific page. It accepts one argument that will be passed as the pageParam property of param object which is the property of context object received infetcher function.If you invoke fetchPage function like this: fetchPage(2); Your fetcher function will received it via pageParam property : const fetcher = ({param: { pageParam }) => {} |
fetchNextPage |
function |
Fetch next page. Your fetcher function will received pageParam via getNextPageParam callback function which is passed as the property of options object. |
fetchPrevPage |
function |
Fetch previous page. Your fetcher function will received pageParam via getPrevPageParam callback function which is passed as the property of options object. |
reset |
function |
Clear cache and reset data . It will invoke onReset callback function passed as the property of options object. |
Usage Example
import { Fragment } from "react";
import { useInfiniteDataQuery } from "../lib/hooks/useInfiniteDataQuery";
const fetchColors = ({ param }) => {
return fetch(`http://.../users?_limit=10&_page=${param.pageParam}`).then((res) => res.json());
};
export default function InfiniteDataQuery() {
const { data: pages, fetchNextPage, hasNextPage, hasPreviousPage, isLoading, isFetching, isFetchingNextPage } = useInfiniteDataQuery("colors", fetchColors, {
getNextPageParam(_lastPage, pages) {
if (pages.length < 4) {
return pages.length + 1;
}
return undefined;
},
getPrevPageParam(_firstPage, pages) {
if (pages.length > 1) {
return pages.length - 1;
}
return undefined;
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{isFetching && <p>Fetching...</p>}
{isFetchingNextPage && <p>Fetching Next Page...</p>}
<div>
{pages?.map((page, i) => (
<Fragment key={i}>
{
page.map((color) => (
<h2 key={color.id}>
{color.id}. {color.label}
</h2>
));
}
</Fragment>
))}
</div>
<div>
<button onClick={fetchNextPage} disabled={!hasNextPage}>Load more</button>
</div>
</div>
);
}
Further explanations with examples will come soon!
A Hook that can be used to fetch data infinitely. Like useInfiniteDataQuery
Hook, will not synchronize data across multiple Hooks with the same dataQueryKey
. This Hook was built on top of useInfiniteDataQuery
. As the name implies, this Hook is just for experimenting. It's not stable yet.
This Hook will invoke
fetchNextPage
function automatically when the scroll position reaches the bottom.
useExperimentalInfiniteScrollDataQuery(
dataQueryKey,
fetcher,
containerRef,
options
);
Parameters
dataQueryKey
- same asuseInfiniteDataQuery
fetcher
- same asuseInfiniteDataQuery
containerRef
- ref created withuseRef
that is passed as theref
prop to the element.
const containeRef = useRef(null);
const queryInstance = useExperimentalInfiniteDataQuery(
dataQueryKey,
fetcher,
containerRef,
options
);
//....
//....
<div ref={containerRef}> ... </div>;
options
- same asuseInfiniteDataQuery
. There is an extra optionoffsetBottom
It can be set any value that can be converted to number. This option is useful when you want to fetch data before the scroll position reaches the bottom most part. It is used in calculation likescrollTop + offsetBottom
.
Return
an object with a set of properties :
Name | Type | Description |
---|---|---|
data |
array |
an array of array that represent pages |
hasNextPage |
boolean |
Indicating whether the data for the next page exists. |
isLoading |
boolean |
Indicating whether the data is being fetched. true for only first time network request. But if you have disabled keepValueOnKeyChanges , this will be set to true if there is no cache for new dataQueryKey . |
isFetching |
boolean |
Indicating whether the data is being fetched. This will always be true on every network requests. |
isFetchingNextPage |
boolean |
Indicating whether the data for the next page is being fetched. |
reset |
function |
Clear cache and reset data . It will invoke onReset callback function passed as the property of options object. |
fetchPage |
function |
Fetch specific page. It accepts one argument that will be passed as the pageParam property of param object which is the property of context object received infetcher function.If you invoke fetchPage function like this: fetchPage(2); Your fetcher function will received it via pageParam property : const fetcher = ({param: { pageParam }) => {} |
Usage Example
import { Fragment, useRef } from "react";
import { useExperimentalInfiniteDataQuery } from "../lib/hooks/useExperimentalInfiniteDataQuery";
const fetchUsers = ({ param }) => {
return fetch(`http://.../users?_limit=5&_page=${param.pageParam}`).then(
(res) => res.json()
);
};
const options = {
offsetBottom: 0,
getNextPageParam(_lastPage, pages) {
if (pages.length < 5) {
return pages.length + 1;
}
return undefined;
},
onReset: (fetchPage) => {
fetchPage(1);
},
};
export default function InfiniteScrollDataQuery() {
const containerRef = useRef(null);
const {
data: usersPages,
isFetchingNextPage,
isFetching,
reset,
} = useExperimentalInfiniteDataQuery(
["users"],
fetchUsers,
containerRef,
options
);
return (
<div style={{ position: "relative" }}>
{(isFetchingNextPage || isFetching) && (
<p style={{ width: "100px", position: "absolute", top: 0, right: 0 }}>
Fetching...
</p>
)}
<div
ref={containerRef}
style={{
height: "600px",
overflowY: "auto",
padding: "10px 20px",
background: "rgba(0, 0, 0, 0.2)",
border: "2px solid red",
}}
>
{usersPages?.map((usersPage, i) => (
<Fragment key={i}>
{usersPage.map((user) => (
<div
key={user.id}
style={{
minHeight: "200px",
display: "flex",
flexDirection: "column",
alignContent: "space-around",
border: "1px solid yellow",
background: "rgba(255, 255, 255, 0.9)",
padding: "10px 15px",
marginBottom: "10px",
}}
>
<h2>
{user.id}. {user.name}
</h2>
<p>{user.alterEgo}</p>
</div>
))}
</Fragment>
))}
</div>
</div>
);
}
Further explanations with examples will come soon!
A Hook that can be used to control caches and network requests. Mostly uses to manage state.
useDataQueryMagic();
No parameters
Return
an object with a set of functions
Function | Parameter | Description |
---|---|---|
setQueryData |
(dataQueryKey , querySetter ) |
Can be used to synchronize data across multiple Hooks with the same dataQueryKey . querySetter can be of any type. If it is a function, that function will be invoked with the data from cache as argument and its return value will be used to set new data for all Hooks with the same dataQueryKey . If is not a function, its value will be used like a returned value of the function. |
setQueriesData |
(dataQueries ) |
Can be used to synchronize multiple data with just one function. dataQueries is an array of dataQuery objects in which must contain dataQueryKey and querySetter properties just like the argument of setQueryData function. |
getQueryData |
(dataQueryKey ) |
Can be used to get data from cache with the same dataQueryKey . |
getQueriesData |
(dataQueryKeys ) |
Get more than one data according to the order list of dataQueryKeys . dataQueryKeys is an array of dataQueryKey . |
cancelQuery |
(dataQueryKey ) |
Cancel ongoing network request with the same dataQueryKey . It is an async function. |
clearCacheData |
(dataQueryKey ) |
Remove cache data associated with your dataQueryKey . |
clearAllCachesData |
( ) | Clear all cache data. |
invalidateQuery |
(dataQueryKey ) |
Invalidate the data from cache, i.e the new network request will be initiated if there is no already ongoing network request associated with same dataQuerykey . Whether or not network request is initiated within the Hook, its status will be updated. such as isFetching : true . |
invalidateQueries |
(dataQueryKeys ) |
Invalidate all data associated with each dataQueryKey of dataQueryKeys array. |
You can use this Hook in optimistic updates
Optimistic Update
Assume that Pets
component display a list of pets.
import { useDataQuery } from "react-data-query";
const fetcher = (context) => { //... your implementations };
const options = { cacheTime: 1000 * 60 * 10, ...};
export const Pets = (props) => {
const { data: pets, isFetching } = useDataQuery(`pets`, fetcher, options);
return (
<div>
{isFetching && <p>Fetching...</p>
{pets?.map(pet => {
// ... return your jsx
});
</div>
)
}
Somewhere in other components or within Pets
component :
const { setDataQuery, getDataQuery, cancelQuery, invalidateQuery } = useDataQueryMagic();
const { isMutating, mutate: addNewPet } = useDataMutation((newPet) => {
return fetch(url, { body: JSON.stringfy(newPet), ... }).then(res => res.json());
},
{
// this callback will be invoked before `mutator` function with the same argument.
onMutate(newPet) {
const prevPets = getDataQuery(`pets`);
// Cancel ongoing network request if it exists. So that there will not be like data conflicts due to optimistic update.
await cancelQuery(`pets`);
// Users will see immediate result before acutal mutation begins
setDataQuery(`pets`, (oldPets) => ({...oldPets, newPet});
// return context object
return { prevPets };
},
onError(_err, _newPet, context) {
// consumes context returned from onMutate callback. If mutation failed, set previous pets.
setDataQuery(`pets`, context.prevPets);
},
onSettled(_newPet, _error, context) {
// invalidate to ensure the data is fresh
invalidateQuery(`pets`);
}
});
const handleAddPet = (newPet) => {
addPet(newPet);
}
Manage State
You can use useDataQuery
and useDataQueryMagic
to synchronize data between multiple components.
Header.jsx
const { setQueryData } = useDataQueryMagic();
// Function that synchronize data(theme) between multiple components
const toggleTheme = () => setQueryData("theme", (light) => !light);
Profile.jsx
const { data: theme } = useDataQuery("theme", undefined, {
autoFetchEnabled: false,
}); // Don't forget to disable autoFetchEnabled
Settings.jsx
const { data: theme } = useDataQuery("theme", undefined, {
autoFetchEnabled: false,
});
Further explanations with examples will come soon!
MIT