Wrapper utilities for CRUD operations in REST APIs entities using RTK Query.
- Install the package:
npm i @ronas-it/rtkq-entity-api
If your app uses axios-observable
, install it along with rxjs
:
npm i axios-observable rxjs
Note that support of axios-observable
will be removed in upcoming major release.
- Create base query with your API configuration, for example using Axios:
import axios from 'axios';
import { createApiCreator, createAxiosBaseQuery } from '@ronas-it/rtkq-entity-api';
const axiosBaseQuery = createAxiosBaseQuery({
getHttpClient: () => axios.create({ baseURL: 'https://your-api-url.com' }),
});
export const createAppApi = createApiCreator({
baseQuery: axiosBaseQuery,
});
- Describe your entity class by extending
BaseEntity
:
import { BaseEntity } from '@ronas-it/rtkq-entity-api';
export class User extends BaseEntity {
name: string;
@Expose({ name: 'phone_number' }) // APIs support of class-trasformer decorators
phoneNumber: string;
constructor(model: Partial<User>) {
super(model);
Object.assign(this, model);
}
}
- Generate your entity API with this creator:
import { createEntityApi } from '@ronas-it/rtkq-entity-api';
import { createAppApi } from 'your-project/utils';
import { User, UserEntityRequest, UserSearchRequest } from 'your-project/models';
export const usersApi = createEntityApi({
// Mandatory params
entityName: 'user', // An entity name. Must by unique
entityConstructor: User, // The entity model class constructor defined above
baseEndpoint: '/users', // Endpoint, relative to base URL configured in the API creator
// Optional params
baseApiCreator: createAppApi, // The APIs creator from above that shares configuration for new APIs
omitEndpoints: ['create', 'update', 'delete'], // Array to specify unimplemented endpoints
entityGetRequestConstructor: UserEntityRequest // Request constructor for 'get' endpoint. Defaults to EntityRequest
entitySearchRequestConstructor: UserSearchRequest, // Request constructor for 'search' endpoint. Defaults to PaginationRequest
});
- Done! Now you can use the api you created as usual one created by RTK Query.
APIs created by createEntityApi
behave as usual ones created by createApi
from RTK Query. Below is the overview of endpoints and utils they provide.
Generated entity APIs provide the following endpoints:
-
create
- this mutation performs aPOST /{baseEndpoint}
request to create an entity. AcceptsPartial
data of entity instance you passed inentityConstructor
when callingcreateEntityApi
. -
get
- query that requestsGET /{baseEndpoint}/{id}
to fetch single entity data. Accepts request params described byentityGetRequestConstructor
-
search
- a query that requestsGET /{baseEndpoint}
to get entities list with pagination. Accepts request params described byentitySearchRequestConstructor
and returnsentitySearchResponseConstructor
extendingPaginationRequest
andPaginationResponse
respectively. -
searchInfinite
- this query behaves similar tosearch
, but accumulates data from newly requested pages. This query can be used withuseSearchInfiniteQuery
hook to implement infinite scrolling lists. It supports loading data in both directions usingfetchNextPage
andfetchPreviousPage
callbacks, and provides other useful props. -
update
- this mutation performsPUT /{baseEndpoint}/{id}
request to update entity data. AcceptsPartial
data of entity instance with mandatoryid
. By default successful call ofupdate
mutation for some entity will patch it's state in all queries where it presented. No further refetch needed. State patch is done my simplemerge
existing data with updated one. -
delete
- a mutation that deletes entities usingDELETE/{baseEndpoint}/{id}
request. Accepts entity ID to delete. On success this mutation will remove the entity from all queries where it was presented without refetching them.
In addition to existing RTKQ utils,
API instances created by createEntityApi
have the following utils in yourApi.util
:
fetchEntity
- util fetches single entity data usingGET /{baseEndpoint}/{id}
with optional params. May be useful in combination with other utilities when customizing onQueryStarted behavior. Example:
// In some mutation in 'someItemApi':
async onQueryStarted(_, { queryFulfilled, dispatch }) {
// Wait for mutation to success:
const { data: createdEntity } = await queryFulfilled;
// Fetch extended entity data:
const someExtendedRequest = { id: createdEntity.id, relations: ['photos'] };
const fullEntity = await someItemApi.util.fetchEntity(
createdEntity.id,
someExtendedRequest,
{ dispatch }
);
// Prefill 'get' query for certain params:
someItemApi.util.upsertQueryData('get', someExtendedRequest, fullEntity);
}
patchEntityQueries
- this utility patches data of an entity in all queries where it is present.
// Some `markAsFavorite` mutation in some `someItemApi`:
markAsFavorite: builder.mutation<void, number>({
query: (id) => ({
method: 'put',
url: `items/${id}/favorite`
}),
async onQueryStarted(id, apiLifecycle}) {
// Perform optimistic entity state patch:
await someItemApi.util.patchEntityQueries(
{ id, isFavorite: true }, // Change `isFavorite` in all occurrences entity in
apiLifecycle,
{
shouldRefetchEntity: false, // Configure whether entity data should be refetched before patch
tags: [{ type: 'item', id: 'favorites' }] // Optionally, pass custom tags of queries to patch
}
);
}
})
clearEntityQueries
- this util can be used to remove some entity data from queries it is presented. Can be useful to perform pessimistic/optimistic deletion. Example of use:
// Some `removeFromFavorite` mutation in some `someItemApi`:
removeFromFavorite: builder.mutation<void, number>({
query: (id) => ({
method: 'delete',
url: `items/${id}/favorite`
}),
async onQueryStarted(id, apiLifecycle}) {
// Wait for mutation to success
await apiLifecycle.queryFulfilled;
// Perform optimistic entity state patch:
await someItemApi.util.clearEntityQueries(
id, // Item ID that was affected
apiLifecycle,
{
tags: [{ type: 'item', id: 'favorites' }] // Remove entity from favorite lists
}
);
}
})
handleEntityUpdate
- this util usespatchEntityQueries
under hood and intended to be used in onQueryStarted callback to perform optimistic/pessimistic update of entity data in queries connected by tags. Example:
// Some `markAsFavorite` mutation in some `someItemApi`:
markAsFavorite: builder.mutation<void, number>({
query: (id) => ({
method: 'put',
url: `items/${id}/favorite`
}),
async onQueryStarted(id, apiLifecycle}) {
// Perform optimistic entity update:
await someItemApi.util.handleEntityUpdate({ id, isFavorite: true }, apiLifecycle, { optimistic: true });
// Or perform pessimistic entity update for specific tags:
await someItemApi.util.handleEntityUpdate({ id, isFavorite: true }, apiLifecycle);
}
})
handleEntityDelete
- this util usesclearEntityQueries
internally and intended to be used in onQueryStarted callback to perform optimistic/pessimistic entity delete fromsearch
-like queries connected by tags. Example:
// Some `removeFromFavorite` mutation in some `someItemApi`:
removeFromFavorite: builder.mutation<void, number>({
query: (id) => ({
method: 'delete',
url: `items/${id}/favorite`
}),
async onQueryStarted(id, apiLifecycle}) {
// Perform optimistic entity delete:
await someItemApi.util.handleEntityDelete(arg, apiLifecycle, { optimistic: true });
// Perform delete pessimistically for specific tags:
await someItemApi.util.handleEntityDelete(arg, apiLifecycle, { tags: [{ type: 'item', id: 'favorites' }] });
}
})
createStoreInitializer
- utility that creates a function, initializing a store for an application. It takes as arguments:rootReducer
,middlewares
(array), andenhancers
(array). This util also contains a helper typeAppStateFromRootReducer<TRootReducer>
for creating the typeAppState
. Example:
// Create the AppState type with the help of AppStateFromRootReducer
export type AppState = AppStateFromRootReducer<typeof rootReducer>;
// Root reducer - an object with the app's different reducers
const rootReducer = {
[authApi.reducerPath]: authApi.reducer,
};
// Array of the app's middlewares
const middlewares = [authApi.middleware];
const initStore = createStoreInitializer({
rootReducer: rootReducer as unknown as Reducer<AppState>,
middlewares,
// Array of the app's enhancers
enhancers: [...reactotronEnhancer],
});
export const store = initStore();
storeActions.init
- a Redux action created for performing actions at the start of the application's lifecycle. It should be dispatched on mount of root application componentApp
:
import { store } from '@your-app/mobile/shared/data-access/store';
import { storeActions } from '@ronas-it/rtkq-entity-api';
import { ReactElement } from 'react';
function App(): ReactElement {
const dispatch = useDispatch();
useEffect(() => {
dispatch(storeActions.init());
}, []);
...
}
function Root(): ReactElement {
return (
<Provider store={store}>
<App />
</Provider>
);
}
And then it can be used in some side effect, for example:
userSettingsListenerMiddleware.startListening({
actionCreator: storeActions.init,
effect: async (_, { dispatch }) => {
const language = await appStorageService.language.get();
language && dispatch(userSettingsActions.setSystemLanguage(language as LanguageCode));
},
});
setupRefetchListeners
is designed for use with React Native applications to automatically refetch data when the app regains focus or reconnects to the internet. It should be in a root component. Before using this utility it's necessary to install@react-native-community/netinfo
.
npm i @react-native-community/netinfo
Example
import { setupRefetchListeners } from '@ronas-it/rtkq-entity-api';
import { useDispatch } from 'react-redux';
function App(): ReactElement {
const dispatch = useDispatch();
useEffect(() => {
const unsubscribeRefetchListeners = setupRefetchListeners(dispatch);
return unsubscribeRefetchListeners;
}, []);
...
}
Warning: setupRefetchListeners
works only in React Native applications.
For web development it's necessary to use setupListeners
from Redux Toolkit.