Usage with Typescript
How to use it with Typescript?
Because redux-requests
has Typescript types for its whole API included, you don't need to do anything to use it with Typescript. However,
there are some special goodies added to make Typescript users even more happy, so it is really worth knowing about them!
RequestAction
interface
As you probably already know, the heart of redux-requests
are so-called requests actions. If you use Typescript, probably you would
like to have them typed! There are 2 ways of doing this:
import { RequestAction } from '@redux-requests/core';
function fetchBooks(): RequestAction { return { type: 'FETCH_BOOKS', request: { url: '/books', }, };}
or...
import { RequestAction } from '@redux-requests/core';
const fetchBooks: () => RequestAction = () => ({ type: 'FETCH_BOOKS', request: { url: '/books', },});
The choice is yours, both are equivalent. Both ways will give you type checking and autocomplete for request action structure, like all
meta
attributes, for example getData
and so on. The true power though could be achieved with generics!
Data
and TransformedData
generics
Before we go on, it is recommended to get familiar with Typescript generics
first if you didn't encounter them before. Going back to Data
and TransformedData
generics, they allow you to describe a structure
of data responded by your server for a given request. Let's see an example:
function fetchBook(id: string): RequestAction<{ id: string; title: string }> { return { type: 'FETCH_BOOK', request: { url: `/books/${id}`, }, };}
By doing so, we just marked that fetchBook
request will respond with an object like { id: '1', title: 'A book' }
. You might be
wondering, what are the benefits? We will get there, but let's start with a simple one:
function fetchBook( id: string,): RequestAction<{ id: string; title: string }, { id: string; name: string }> { return { type: 'FETCH_BOOK', request: { url: `/books/${id}`, }, meta: { getData: data => ({ id: data.id, name: data.title }), }, };}
As you can see, we passed a 2nd generic called TransformedData
and implemented meta.getData
to replace title
attribute
with name
. As you probably know, getData
allows to change a server response data to fit your need. Adding Data
and TransformedData
generics here made getData
automatically typed - that's it - Typescript will recognize that data
has { id: string, title: string }
type and getData
response has { id: string, name: string }
type. This gives you very good confidence, that getData
transformation
is done correctly, for instance data => ({ id: data.id, name2: data.title })
would immediately show error because TransformedData
doesn't have name2
attribute.
Interestingly, TransformedData
defaults to Data
, so passing RequestAction<{ title: string }>
is the same as passing
RequestAction<{ title: string }, { title: string }>
.
Ok, but if we don't use getData
, why would we even care about those generics? It turns out that it is very useful to
define Data
generic in all cases, because then you will enjoy an automatic type inference for the whole application!
Before we will learn about it, first let's learn how to use Typescript with selectors!
getQuery
and getQuerySelector
generics
If you need to read a state from Redux store, you usually do this with selectors. As you probably know, redux-requests
has
several optimized selectors built-in. Let's use them to read book data:
import { getQuery } from '@redux-requests/core';
const { data, loading, error } = getQuery(state, { type: 'FETCH_BOOK' });
Ok, but it would be cool to have data
typed, wouldn't it? Let's fix that by a generic:
const { data, loading, error } = getQuery<{ id: string; title: string }>( state, { type: 'FETCH_BOOK' },);
That's better, now data
is typed properly! Let's see how it would be done with getQuerySelector
:
import { getQuerySelector } from '@redux-requests/core';
const bookSelector = getQuerySelector<{ id: string; title: string }>({ type: 'FETCH_BOOK',});const { data, loading, error } = bookSelector(state);
Now, imagine you need an access to a given request data type in multiple places. Wouldn't it be better to pass a generic once
and forget about it? This is where automatic data inference comes into play and it answers the question why it is useful
to provide Data
generic to request actions!
Automatic data inference
So, how does it work in practice? Let's go back to fetchBook
action. It has already defined data
type. It turns out that
generics of selectors and actions are connected, so instead of passing a generic in selectors, you can just pass actions as action
prop:
const { data, loading, error } = getQuery(state, { type: 'FETCH_BOOK', action: fetchBook,});
or... with getQuerySelector
:
const bookSelector = getQuerySelector({ type: 'FETCH_BOOK', action: fetchBook,});const { data, loading, error } = bookSelector(state);
The downside is, that you need to provide both action and constant, which is not perfect. But, there is a way to fix that! Just use an action creator library and forget about constants!
Using with action creator library
Before showing how to use an action creator library with Typescript, see general guide
if you didn't already! One you have done that, we will show below how combine redux-smart-actions
with Typescript and how it could help us with Data
generics:
import { createSmartAction } from 'redux-smart-actions';
const fetchBook = createSmartAction(function ( id: string,): RequestAction<{ id: string; title: string }, { id: string; name: string }> { return { request: { url: `/books/${id}`, }, meta: { getData: data => ({ id: data.id, name: data.title }), }, };});
or with a slightly different syntax...
import { createSmartAction } from 'redux-smart-actions';
const fetchBook: ( id: string,) => RequestAction< { id: string; title: string }, { id: string; name: string }> = createSmartAction(id => { return { request: { url: `/books/${id}`, }, meta: { getData: data => ({ id: data.id, name: data.title }), }, };});
Notice that we don't have type
anymore! fetchBook.toString() === 'FETCH_BOOK'
, so now you can pass it
in all redux-requests
functions wherever you would pass FETCH_BOOK
type! For example:
const { data, loading, error } = getQuery(state, { type: fetchBook,});
That's it! We don't need action
prop anymore as type
already has it! We needed to define type for data only in fetchBook
, you can pass it in selectors in mupltiple places and have data
typed automatically!
What is interesting, you could even have this code in js
file, still your editor like vscode
would show you data
structure, so
this means that you could use Typescript only in actions files but javascript in others and still have data
autocompletion!
RequestsStore
and dispatchRequest
As always, in order to create a request, you must dispatch a request action, for instance:
const { data, error } = await store.dispatch(fetchBook('1'));
There is a problem though, dispatch
is not properly typed, because the official Redux
types for dispatch
cannot know about
middleware from this library, which returns a promise with server response for dispatched request actions.
Fortunately, in all places you would dispatch request actions, you could use RequestsStore
and its dispatchRequest
method:
import { createRequestsStore } from '@redux-requests/core';
const requestsStore = createRequestsStore(store);const { data, error } = await requestsStore.dispatchRequest(fetchBook('1'));
Now, result of dispatchRequest
is properly typed, and as a bonus, if you defined Data
generic in dispatched
action, also data
will be typed! Again, automatic type inference!
Regarding functionality, createRequestsStore
doesn't do anything else than normal store, it just decorates passed store
with dispatchRequest
method which is just a copy of normal dispatch
. So, dispatchRequest
does exactly the same thing
as dispatch
, the only difference is that dispatchRequest
is properly typed.
What's interesting, in all interceptors you have access to RequestsStore
instead of Store
, so you already could utilize
dispatchRequest
there.
ResponseData
utility type
Sometimes you might want to use a type which is just a Data
generic used in a request action. For your convenience, instead
of checking and worrying about it, you can just get it from a request action, for example:
import { ResponseData } from '@redux-requests/core';
type BookData = ResponseData<typeof fetchBook>;// the same as type BookData = { id: string; name: string }
LocalMutationAction
If you use a local mutation, you could also use LocalMutationAction
interface:
import { LocalMutationAction } from '@redux-requests/core';
function updateBookName(id: string, newName: string): LocalMutationAction { return { type: 'UPDATE_BOOK_NAME', meta: { mutations: { FETCH_BOOK: { updateData: data => data && data.id === id ? { ...data, name: newName } : data, local: true, }, }, }, };}
...or if fetchBook
is normalized, you could just:
import { LocalMutationAction } from '@redux-requests/core';
function updateBookName(id: string, newName: string): LocalMutationAction { return { type: 'UPDATE_BOOK_NAME', meta: { localData: { id, name: newName }, }, };}
Usage with React
If you use React, useQuery
and Query
also have action
prop, and also you could pass a request action as type
if
you use a library like redux-smart-actions
- in the similar fashion as in getQuery
and getQuerySelector
.
Also, instead of useDispatch
from react-redux
, you can use useDispatchRequest
:
import { useDispatchRequest } from '@redux-requests/react';
const BookFetcher = () => { const dispatchRequest = useDispatchRequest();
return ( <button onClick={async () => { const { data, error } = await dispatch(fetchBook('1')); }} > Fetch book </button> );};
Then you could enjoy data
inference like in dispatchRequest
from RequestsStore
. From functionality perspective though,
useDispatchRequest
is just reexported useDispatch
from react-redux
, so it works exactly the same.