react-redux-query
v0.10.0
Published
React hooks and functions for SWR-style data fetching, backed by Redux
Downloads
74
Maintainers
Readme
react-redux-query
A few hooks and functions for declarative data fetching, caching, sharing, automatic updates, and request deduplication. Like SWR and React Query, but uses Redux for persistence.
Flexible, small and simple. Written in TypeScript.
Installation
npm i react-redux-query
or yarn add react-redux-query
.
Usage
RRQ's main hook, useQuery
, fetches data, throws it into Redux, and rerenders your components whenever data changes.
It takes 3 arguments, (key: string, fetcher: () => Promise<{}>, options?: {})
. It calls your fetcher and immediately returns the cached data in Redux at key
. It connects your component to Redux with useSelector
, so it subscribes to data changes whenever they occur. This means your component always rerenders with the most recently fetched data at key
.
import { useQuery } from 'react-redux-query'
function Profile() {
const { data } = useQuery('user', service.getLoggedInUser)
if (!data) return <div>Loading...</div>
return <div>Hello {data.name}!</div>
}
If you want to make sure you don't throw an error into Redux and overwrite good data with bad, you can have your fetcher return null
or undefined
:
function Profile() {
const { data } = useQuery('user', async () => {
const res = await service.getLoggedInUser()
return res.status === 200 ? res : null
})
// ...
}
Or you can set the queryData
property in the object returned by fetcher
:
function Profile() {
const { data, error } = useQuery(
'user',
async () => {
const res = await service.getLoggedInUser()
return { ...res, queryData: res.status === 200 ? res : null }
},
{ stateKeys: ['error'] },
)
// ...
}
This way you can return the unmodified response from your fetcher, even if it's a "bad" response, while instructing RRQ to not overwrite your data
in Redux. In this case, the error
variable would contain the response for status codes other than 200
, or an error object if fetcher throws an error.
If you don't want useQuery
to call the fetcher, just pass null
or undefined
for either the key or the fetcher.
Setup
RRQ uses Redux to cache fetched data, and allows components to subscribe to changes in fetched data. To use RRQ in your app, you need to use Redux.
import { combineReducers, createStore } from 'redux'
import { Provider } from 'react-redux'
import { reducer as queryReducer } from 'react-redux-query'
const rootReducer = combineReducers({ ...myOtherReducers, query: queryReducer })
const store = createStore(rootReducer, {})
// ...
const App = () => {
return (
<Provider store={store}>
<MyApp />
</Provider>
)
}
The default name of the RRQ branch in your Redux state tree is
'query'
. See below for how to use a custom branch name.
Polling
To do polling with useQuery
, just pass the intervalMs
property in the options. After fetcher
returns, it's called again after intervalMs
. The actual polling interval depends on how long fetcher takes to return, which means polling interval adapts to network and server speed.
When polling, to ensure the fetcher is redefined each time it's called, useQuery
updates a piece of state that forces its component to rerender. If you have a fetcher that only needs to be defined once, and you want to avoid an extra rerender each time it's called, pass false
for intervalRedefineFetcher
in the options.
query
function
RRQ also exports a lower-level async query
function that has the same signature as useQuery
: (key: string, fetcher: () => Promise<{}>, options: {})
.
This function is used by useQuery
. It calls fetcher
, awaits the response, throws data into Redux if appropriate, and returns the response as-is.
You should use this function wherever you want to fetch and cache data outside of lifecycle methods. For example, in a save user callback:
import { query } from 'react-redux-query'
const handleSaveUser = async (userId) => {
await saveUser(userId)
const res = await query(`user/${userId}`, () => fetchUser(userId), { dispatch })
if (res.status !== '200') {
handleError()
}
}
Because it's not a hook, the
query
function also lets you use RRQ in class components. After you throw data into Redux, you can read it out of the query branch and pass it to your components inmapStateToProps
.
The options
object must contain a dispatch
property with the Redux dispatch function (this is used to throw data into Redux). Feel free to write a wrapper around query
that passes in dispatch
for you if you don't want to pass it every time.
useQueryState
hook
If you just want to subscribe to data changes without sending a request, use the useQueryState
hook (which is used by useQuery
under the hood).
It takes a key
and an options
object (it omits the fetcher
). It connects your component to Redux and returns the query state object at key
, with a subset of properties specified by options.stateKeys
. To avoid unnecessary rerenders, only data
and dataMs
are included by default.
You can pass an array of additional keys ('error'
, 'errorMs'
, 'fetchMs'
, 'inFlight'
) to subscribe to changes in these properties as well.
To control whether your component rerenders when query state changes, you can pass in a custom equality comparator using options.compare
. This function takes previous query state and next query state as args. If it returns false, your connected component rerenders, else it doesn't. It uses shallowEqual
by default, which means any change in data
triggers a rerender.
Redux actions
RRQ ships with the following Redux actions:
save
: saves data at keyupdate
: like save, but takes an updater function, which receives thedata
at key and must return updated data,undefined
, ornull
; returningundefined
is a NOOP, while returningnull
removes query state object at key from query branchupdateQueryState
: updates query state object (you probably don't need to use this)
These are really action creators (functions that return action objects). You can use the first two to overwrite the data
at a given key in the query branch. For example, in a save user callback:
import { update } from 'react-redux-query'
const handleSaveUser = async (userId, body) => {
const res = await saveUser(userId, body)
dispatch(
update({
key: `user/${userId}`,
updater: (prevData) => {
return { ...prevData, ...res }
},
}),
)
}
All useQuery
options
intervalMs
: Interval between end of fetcher call and next fetcher callintervalRedefineFetcher
: If true, fetcher is redefined each time it's called on interval, by forcing component to rerender (false by default)noRefetch
: If true, don't refetch if there's already data at keynoRefetchMs
: If noRefetch is true, noRefetch behavior active for this many ms (forever by default)refetchKey
: Pass in new value to force refetch without changing keyupdater
: If passed, this function takes data currently at key, plus data in response, and returns updated data to be saved at keysaveStaleResponse
: If true, save response even if it's "stale", i.e. request'sfetchMonoMs
<queryState.goodfetchMonoMs
(false by default)dedupe
: If true, don't call fetcher if another request was recently sent for keydedupeMs
: If dedupe is true, dedupe behavior active for this many ms (2000 by default)catchError
: If true, any error thrown by fetcher is caught and assigned to queryState.error property (true by default)stateKeys
: Additional keys in query state to include in return value (only data and dataMs included by default)compare
: Equality function compares previous query state with next query state; if it returns false, component rerenders, else it doesn't; uses shallowEqual by default
Custom config context
RRQ's default behavior can be configured using ConfigContext
, which has the following properties and default values:
branchName?: string // 'query'
dedupe?: boolean // false
dedupeMs?: number // 2000
saveStaleResponse?: boolean // false
catchError?: boolean // true
compare?: (prev: QueryState, next: QueryState) => boolean // shallowEqual
intervalRedefineFetcher?: boolean // false
Import ConfigContext
, and wrap any part of your render tree with ConfigContext.Provider
:
import { ConfigContext } from 'react-redux-query'
// ...
;<ConfigContext.Provider value={{ branchName: 'customQueryBranchName', catchError: false }}>
<MyApp />
</ConfigContext.Provider>
ConfigContext
uses React's Context API. This config applies to all hooks in your app under the context provider.
Full API
RRQ's codebase is small and thoroughly documented.
For doc comments, function signatures and type definitions, see here.
For action creators, see here.
TypeScript
react-redux-query works great with TypeScript (it's written in TypeScript).
Make sure you enable esModuleInterop
if you're using TypeScript to compile your application. This option is enabled by default if you run tsc --init
.
Why react-redux-query?
Why not SWR or React Query?
- RRQ uses Redux for data persistence and automatic updates; performant, community-standard solution for managing application state; easy to modify and subscribe to stored data, and easy to extend RRQ's read/write behavior by writing your own hooks/selectors/actions
queryData
property makes it easy to transform fetcher response before caching it, or instruct RRQ not to cache data at all, without changing shape of response or making it null- First class TypeScript support; RRQ is written in TypeScript, and argument/return types are seamlessly inferred from fetcher return types
- Not only hooks;
query
function means RRQ can be used outside of lifecycle methods, or in class components - Small and simple codebase; RRQ weighs less than 3kb minzipped
Dependencies
React and Redux.
Development and tests
Clone the repo, then yarn
, then yarn test
. This runs tests on the vanilla JS parts of RRQ, but none of the React code.
To test the React code, run cd test_app
, then yarn
.
Then run yarn start
or yarn test
to run React test app or to run tests on test app.