Querier
Simple declarative data layer for React applications
Table of Content
- What is Querier?
- Querier gist
- Getting data in and out from component
- Handling query statuses
- Caching
- Server-side rendering
- API
- Known limitations
What is Querier?
Querier is a tool that helps you get remote and async data into React app effortlessly.
With simple, declarative API, you don't have to worry about handling async state or errors. All you have to do is decorate component that requires async data using withData
higher-order component. Results, states and possible errors will be injected into the component as props.
Querier is implemented and tested in TypeScript.
What is a declarative data layer?
Simply, it is a way to supply data to your components. You declare component's data requirements, and the whole logic for fetching it, supplying status and results is handled by Querier itself. To achieve that, you implement queries
. Queries are simply async functions. They can either communicate with remote source or resolve async data in whatever way you imagine.
Querier Gist
First, define your queries:
// queries.js const getRepository = async { const results = await octokitrepos; return resultsdata;}; const starRepository = async { const results = await octokitactivity; return resultsdata;};
Second, make your component use the queries:
import getRepository starRepository from 'queries.js';import withData QuerierState from 'querier'; // Define component that will require async dataComponent { const repository = thispropsresults; ifrepository // action queries are available via actionQueries prop thispropsactionQueries; } { const name = thispropsresultsrespository; const states: repository: state = thisprops; return state === QuerierStateSuccess ? <div> <h1>Respository name: name</h1> <button =>Star repo</button> </div> : null; } // Define RepositoryDetails data requirements (vide queries)const repositoryDetailsQueries = inputQueries: repository: query: getRepository actionQueries: starRepository: query: starRepository ; // Decorate your component using withData HOCrepositoryDetailsQueriesRepositoryDetails;
For examples take a look into examples
directory.
Queries
Queries gives you ability to get data in and out of React component. They are the most important part of Querier.
To supply data into component you use inputQueries
. To perform async operations (e.g. submittng a form) you use actionQueries
.
Input queries
Here's an example of a simple input query:
// queries.ts ;
To make getRepository
query supply data to your component, you need to decorate your component using withData
HOC:
// YourComponent.tsx // First, define data dependencies definition // Second, use withData HOC;
Input queries executors - manually invoking input queries
Input queries are automatically invoked when component's props change. However, in some cases there is a need to re-invoke input query for the same set of props. To do so, each query is passed to the component in inputQueries
property, that contain each query's executor. In the case of the example above, the executor for repository
input query is accessible by:
thispropsinputQueriesrepository
Input queries key notes
- Input queries are fired when component mounts
- Input queries always receive component's full set of props as argument
- Input queries are being reinvoked when component's props change
- Input query results are cached by default. See Caching for more details.
Action queries
Action queries are similiar to input queries. The main difference is that thay are not being invoked automatically.
The purpose of action query is to perform async operation as a result of user interaction, e.g. submitting a form.
Here's an example of action query usage:
// queries.ts ;
// SearchComponent.tsx ;
Action queries key notes
- Results of the action query will be passed back to the component. Thanks to that you can declaratively perform e.g. navigation using React Router.
- Action queries results are cached by default. See Caching for more details.
Data dependencies definition
In order to perform queries you need to define them and pass to withData
HOC. Data dependency definition is a simple JSON object describing queries your component will use:
; ; RepositoryDetailsProps, RepositoryDetailsInputQueries, RepositoryDetailsActionQueries> =;
Data dependency definition is described by type DataDependencyDefinition<TProps, TInputQueries, TActionQueries>
:
;
Input and action queries are defined separately, and each query definition accepts following options:
Query defintion options
hot?: boolean
- when set totrue
makes the query not cacheable. See Caching for more informationlazy?: boolean
- Input queries only. When set totrue
, input query will not be fired on mountresultActions?: Array<ActionFunction1<TQueryResult, Action<TQueryResult>>>
- array of Redux action creators. When defined, these actions will be performed when query succeedes. Use it e.g. when you want to store your query result in Redux store.
Handling query statuses
When decorating your component using withData
HOC all your queries statuses are passed to the component in states
prop. Each query state is represented by QueryStateType
:
;
Let's assume you declared, that your component will use getUsers
input query result as users
prop. This means, that the states
prop will look like this:
Having this knowledge you can start building UI abstractions over this API to display e.g. feedback indicators to your users. Or, in case of action queries, you can for example redirect user to a new page when action query suceedes.
Also, there is a combineStates(states: StatesType): QueryStateType
utility that will calculate derived status of multiple query states.
Caching
By default, all input queries executed by Querier are cached. However, there are cases, when you want your queries to be executed every time instead of served from cache. To do so you need to declare your query as hot
in your data dependencies definition passed to withData
HOC:
For action queriers set hot: false
to make them cacheable.
Querier cache is not persistent - you need to take care of this by yourself. QuerierProvider
component accepts querier: QuerierType
property that can be initialised using querier store that you have cached in e.g. local storage.
Server-side rendering
For SSR example please take a look at example repository
API
Querier
Querier(store?: QuerierStoreType, dispatch?: Dispatch<{}>)
is the core part of the tool. It's responsibility is to perform queries, cache and pass them back to components.
Arguments
store
is an object, when performed queries are stored. It keeps queries ids, states, results, and reasons(components that caused the query invocation). You can initialise Querier using store you have e.g. build during server-side rendering or store in local storage.dispatch
is Redux's dispatch function. Initialise Querier withdispatch
if you want to performresultActions
on your queries. See Query defintion options for more details
QuerierProvider
<QuerierProvider querier>
Makes querier available to components decorated with withData
HOC. QuerierProvider
should be the topmost component in your React tree.
Props
querier?: QuerierType
- passquerier
prop if you have e.g. built your Querier store during server-side rendering.
withData
withData(dependencies)
return HOC that connects your component to Querier.
Arguments
dependencies: DataDependencyDefinition<TProps, TInputQueries, TActionQueries>
- please refer to Data dependencies definition for details
## Known limitations
Action queries arguments typings
Action queries arguments are not typed at the moment
module: 'commonjs'
set is problematic
Anonymous query function exports in TypeScript when Query keys in Querier cache are based on query function name and params passed to the query. When using module: 'commonjs'
Typescript turns this :
export const someQuery = () => 1
into this:
exports { ...
someQuery.name
will return empty string for such export, meaning, that if your component rely on multiple input queries, or multiple queries rely on the same set or props, then all off them will resolve with data stored in cache for first query that finished execution. This of course leads to invalid data :(
To avoid this situation you can either set module
to esnext
or define your queries as non-anonymous functions or functions assigned to a const
end exported later:
// This will work: // This will work as well:;