Wrapper utilities for CRUD operations in REST APIs entities using RTK Query.
- Install the package:
npm i @ronas-it/rtkq-entity-api- 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. AcceptsPartialdata of entity instance you passed inentityConstructorwhen 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 byentitySearchRequestConstructorand returnsentitySearchResponseConstructorextendingPaginationRequestandPaginationResponserespectively. -
searchPaginated- this query behaves similar tosearch, but accumulates data from newly requested pages. This query can be used withuseSearchPaginatedInfiniteQueryhook to implement infinite scrolling lists. It supports loading data in both directions usingfetchNextPageandfetchPreviousPagecallbacks, and provides other useful props. -
update- this mutation performsPUT /{baseEndpoint}/{id}request to update entity data. AcceptsPartialdata of entity instance with mandatoryid. By default successful call ofupdatemutation for some entity will patch it's state in all queries where it presented. No further refetch needed. State patch is done my simplemergeexisting 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 usespatchEntityQueriesunder 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 usesclearEntityQueriesinternally 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));
},
});setupRefetchListenersis 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/netinfoExample
import { addEventListener, fetch } from '@react-native-community/netinfo';
import { setupRefetchListeners } from '@ronas-it/rtkq-entity-api';
import ReactNative from 'react-native';
import { useDispatch } from 'react-redux';
function App(): ReactElement {
const dispatch = useDispatch();
useEffect(() => {
const unsubscribeRefetchListeners = setupRefetchListeners(
dispatch,
{ refetchOnFocus: true, refetchOnReconnect: true },
{ addEventListener, fetch },
ReactNative,
);
return unsubscribeRefetchListeners;
}, []);
...
}Warning: setupRefetchListeners works only in React Native applications.
For web development it's necessary to use setupListeners
from Redux Toolkit.